Skip to content
Commits on Source (3)
......@@ -59,6 +59,9 @@ Sorted alphabetically by repo name.
- The q2-diversity plugin
https://github.com/qiime2/q2-diversity/issues
- The q2-diversity-lib plugin
https://github.com/qiime2/q2-diversity-lib/issues
- The q2-emperor plugin
https://github.com/qiime2/q2-emperor/issues
......
......@@ -52,6 +52,8 @@ Sorted alphabetically by repo name.
| The q2-demux plugin
- [q2-diversity](https://github.com/qiime2/q2-diversity/issues)
| The q2-diversity plugin
- [q2-diversity-lib](https://github.com/qiime2/q2-diversity-lib/issues)
| The q2-diversity-lib plugin
- [q2-emperor](https://github.com/qiime2/q2-emperor/issues)
| The q2-emperor plugin
- [q2-feature-classifier](https://github.com/qiime2/q2-feature-classifier/issues)
......
......@@ -25,6 +25,7 @@ requirements:
- python-dateutil
- pyparsing ==2.3.0
- bibtexparser
- networkx
test:
imports:
......
qiime (2019.4.0-1) UNRELEASED; urgency=medium
* New upstream version
-- Liubov Chuprikova <chuprikovalv@gmail.com> Tue, 11 Jun 2019 19:52:15 +0200
qiime (2019.1.0-2) unstable; urgency=medium
* Team upload.
......
......@@ -23,9 +23,9 @@ def get_keywords():
# setup.py/versioneer.py will grep for the variable names, so they must
# each be defined on a line of their own. _version.py will just call
# get_keywords().
git_refnames = " (tag: 2019.1.0)"
git_full = "58cb1e54f57e10fdbf9b032d8192e6089123fed4"
git_date = "2019-01-29 14:00:34 +0000"
git_refnames = " (tag: 2019.4.0)"
git_full = "f60a7d43a97f065d4e3248e3df93703aa48d95ab"
git_date = "2019-05-03 04:14:45 +0000"
keywords = {"refnames": git_refnames, "full": git_full, "date": git_date}
return keywords
......
......@@ -135,3 +135,12 @@ class UnimportableFormat(TextFileFormat):
UnimportableDirectoryFormat = model.SingleFileDirectoryFormat(
'UnimportableDirectoryFormat', 'ints.txt', UnimportableFormat)
class EchoFormat(TextFileFormat):
def _validate_(self, level):
pass # Anything is a valid echo file
EchoDirectoryFormat = model.SingleFileDirectoryFormat(
'EchoDirectoryFormat', 'echo.txt', EchoFormat)
# ----------------------------------------------------------------------------
# Copyright (c) 2016-2019, QIIME 2 development team.
#
# Distributed under the terms of the Modified BSD License.
#
# The full license is in the file LICENSE, distributed with this software.
# ----------------------------------------------------------------------------
import os
from .plugin import dummy_plugin, C1, C2, C3, Foo, Bar, Baz, EchoFormat
from qiime2.plugin import (
TypeMap, TypeMatch, Properties, Visualization, Bool, Choices)
def constrained_input_visualization(output_dir: str, a: EchoFormat,
b: EchoFormat):
with open(os.path.join(output_dir, 'index.html'), 'w') as fh:
fh.write("<p>%s</p>" % a.path.read_text())
fh.write("<p>%s</p>" % b.path.read_text())
T, U, V = TypeMap({
(Foo, Foo): Visualization,
(Bar, Bar): Visualization,
(Baz, Baz): Visualization,
(C1[Foo], C1[Foo]): Visualization,
(C1[Bar], C1[Bar]): Visualization,
(C1[Baz], C1[Baz]): Visualization
})
dummy_plugin.visualizers.register_function(
function=constrained_input_visualization,
inputs={
'a': T,
'b': U
},
parameters={},
name="Constrained Input Visualization",
description="Ensure Foo/Bar/Baz match"
)
del T, U, V
def combinatorically_mapped_method(a: EchoFormat, b: EchoFormat
) -> (EchoFormat, EchoFormat):
return a, b
T, R = TypeMap({
Foo: Bar,
Bar: Baz,
Baz: Foo
})
X, Y = TypeMap({
C3[Foo | Bar | Baz, Foo | Bar | Baz, Foo]: Foo,
C3[Foo | Bar | Baz, Foo | Bar | Baz, Bar]: Bar,
C3[Foo | Bar | Baz, Foo | Bar | Baz, Baz]: Baz
})
dummy_plugin.methods.register_function(
function=combinatorically_mapped_method,
inputs={
'a': C1[T],
'b': X
},
parameters={},
outputs=[
('x', C2[R, R]),
('y', Y)
],
name="Combinatorically Mapped Method",
description="Test that multiple typemaps can be used"
)
del T, R, X, Y
def double_bound_variable_method(a: EchoFormat, b: EchoFormat,
extra: EchoFormat) -> EchoFormat:
return extra
T, R = TypeMap({
Foo: Bar,
Bar: Baz,
Baz: Foo
})
dummy_plugin.methods.register_function(
function=double_bound_variable_method,
inputs={
'a': T,
'b': T,
'extra': Foo
},
parameters={},
outputs=[
('x', R)
],
name="Double Bound Variable Method",
description="Test reuse of variables"
)
del T, R
def bool_flag_swaps_output_method(a: EchoFormat, b: bool) -> EchoFormat:
return a
P, R = TypeMap({
Choices(True): C1[Foo],
Choices(False): Foo
})
dummy_plugin.methods.register_function(
function=bool_flag_swaps_output_method,
inputs={
'a': Bar
},
parameters={
'b': Bool % P
},
outputs=[
('x', R)
],
name='Bool Flag Swaps Output Method',
description='Test if a parameter can change output'
)
del P, R
def predicates_preserved_method(a: EchoFormat) -> EchoFormat:
return a
P = TypeMatch([Properties('A'), Properties('B'), Properties('C'),
Properties('X', 'Y')])
dummy_plugin.methods.register_function(
function=predicates_preserved_method,
inputs={
'a': Foo % P
},
parameters={},
outputs=[
('x', Foo % P)
],
name='Predicates Preserved Method',
description='Test that predicates are preserved'
)
del P
......@@ -10,7 +10,7 @@ from importlib import import_module
from qiime2.plugin import (Plugin, Bool, Int, Str, Choices, Range, List, Set,
Visualization, Metadata, MetadataColumn,
Categorical, Numeric)
Categorical, Numeric, TypeMatch)
from .format import (
IntSequenceFormat,
......@@ -23,11 +23,13 @@ from .format import (
FourIntsDirectoryFormat,
RedundantSingleIntDirectoryFormat,
UnimportableFormat,
UnimportableDirectoryFormat
UnimportableDirectoryFormat,
EchoFormat,
EchoDirectoryFormat
)
from .type import (IntSequence1, IntSequence2, Mapping, FourInts, SingleInt,
Kennel, Dog, Cat)
Kennel, Dog, Cat, C1, C2, C3, Foo, Bar, Baz)
from .method import (concatenate_ints, split_ints, merge_mappings,
identity_with_metadata, identity_with_metadata_column,
identity_with_categorical_metadata_column,
......@@ -61,12 +63,13 @@ import_module('qiime2.core.testing.transformer')
# Register semantic types
dummy_plugin.register_semantic_types(IntSequence1, IntSequence2, Mapping,
FourInts, Kennel, Dog, Cat, SingleInt)
FourInts, Kennel, Dog, Cat, SingleInt,
C1, C2, C3, Foo, Bar, Baz)
# Register formats
dummy_plugin.register_formats(
IntSequenceFormatV2, MappingFormat, IntSequenceV2DirectoryFormat,
MappingDirectoryFormat)
MappingDirectoryFormat, EchoDirectoryFormat, EchoFormat)
dummy_plugin.register_formats(
FourIntsDirectoryFormat, UnimportableDirectoryFormat, UnimportableFormat,
......@@ -102,6 +105,17 @@ dummy_plugin.register_semantic_type_to_format(
artifact_format=MappingDirectoryFormat
)
dummy_plugin.register_semantic_type_to_format(
C3[C1[Foo | Bar | Baz] | Foo | Bar | Baz,
C1[Foo | Bar | Baz] | Foo | Bar | Baz,
C1[Foo | Bar | Baz] | Foo | Bar | Baz]
| C2[Foo | Bar | Baz, Foo | Bar | Baz]
| C1[Foo | Bar | Baz | C2[Foo | Bar | Baz, Foo | Bar | Baz]]
| Foo
| Bar
| Baz,
artifact_format=EchoDirectoryFormat)
# TODO add an optional parameter to this method when they are supported
dummy_plugin.methods.register_function(
function=concatenate_ints,
......@@ -123,17 +137,16 @@ dummy_plugin.methods.register_function(
citations=[citations['baerheim1994effect']]
)
# TODO update to use TypeMap so IntSequence1 | IntSequence2 are accepted, and
# the return type is IntSequence1 or IntSequence2.
T = TypeMatch([IntSequence1, IntSequence2])
dummy_plugin.methods.register_function(
function=split_ints,
inputs={
'ints': IntSequence1
'ints': T
},
parameters={},
outputs=[
('left', IntSequence1),
('right', IntSequence1)
('left', T),
('right', T)
],
name='Split sequence of integers in half',
description='This method splits a sequence of integers in half, returning '
......@@ -400,7 +413,7 @@ dummy_plugin.visualizers.register_function(
inputs={},
parameters={
'name': Str,
'age': Int
'age': Int % Range(0, None)
},
name='Parameters only viz',
description='This visualizer only accepts parameters.'
......@@ -596,3 +609,5 @@ dummy_plugin.pipelines.register_function(
description=('This is useful to make sure all of the intermediate stuff is'
' cleaned up the way it should be.')
)
import_module('qiime2.core.testing.mapped')
# ----------------------------------------------------------------------------
# Copyright (c) 2016-2019, QIIME 2 development team.
#
# Distributed under the terms of the Modified BSD License.
#
# The full license is in the file LICENSE, distributed with this software.
# ----------------------------------------------------------------------------
# ----------------------------------------------------------------------------
# Copyright (c) 2016-2019, QIIME 2 development team.
#
# Distributed under the terms of the Modified BSD License.
#
# The full license is in the file LICENSE, distributed with this software.
# ----------------------------------------------------------------------------
import unittest
from qiime2 import Artifact
from qiime2.core.testing.util import get_dummy_plugin
class ActionTester(unittest.TestCase):
ACTION = 'N/A'
def setUp(self):
plugin = get_dummy_plugin()
self.action = plugin.actions[self.ACTION]
def run_action(self, **inputs):
results = self.action(**inputs)
future = self.action.asynchronous(**inputs)
async_results = future.result()
for a, b in zip(async_results, results):
self.assertEqual(a.type, b.type)
return results
class TestConstrainedInputVisualization(ActionTester):
ACTION = 'constrained_input_visualization'
def test_match_foo(self):
a = Artifact.import_data('Foo', "element 1", view_type=str)
b = Artifact.import_data('Foo', "element 2", view_type=str)
viz, = self.run_action(a=a, b=b)
contents = (viz._archiver.data_dir / 'index.html').read_text()
self.assertIn('element 1', contents)
self.assertIn('element 2', contents)
def test_match_nested(self):
a = Artifact.import_data('C1[Baz]', "element 1", view_type=str)
b = Artifact.import_data('C1[Baz]', "element 2", view_type=str)
viz, = self.run_action(a=a, b=b)
contents = (viz._archiver.data_dir / 'index.html').read_text()
self.assertIn('element 1', contents)
self.assertIn('element 2', contents)
def test_mismatch_foo_bar(self):
a = Artifact.import_data('Foo', "element 1", view_type=str)
b = Artifact.import_data('Bar', "element 2", view_type=str)
with self.assertRaisesRegex(ValueError, 'No solution.*Foo'):
viz, = self.run_action(a=a, b=b)
def test_mismatch_nested(self):
a = Artifact.import_data('C1[Foo]', "element 1", view_type=str)
b = Artifact.import_data('Foo', "element 2", view_type=str)
with self.assertRaisesRegex(ValueError, 'No solution.*C1'):
viz, = self.run_action(a=a, b=b)
class TestCombinatoricallyMappedMethod(ActionTester):
ACTION = 'combinatorically_mapped_method'
def test_match_foo(self):
a = Artifact.import_data('C1[Foo]', 'element 1', view_type=str)
b = Artifact.import_data('C3[Foo, Foo, Foo]',
'element 2', view_type=str)
x, y = self.run_action(a=a, b=b)
self.assertEqual(repr(x.type), 'C2[Bar, Bar]')
self.assertEqual(repr(y.type), 'Foo')
def test_match_bar_foo(self):
a = Artifact.import_data('C1[Bar]', 'element 1', view_type=str)
b = Artifact.import_data('C3[Foo, Foo, Foo]',
'element 2', view_type=str)
x, y = self.run_action(a=a, b=b)
self.assertEqual(repr(x.type), 'C2[Baz, Baz]')
self.assertEqual(repr(y.type), 'Foo')
def test_match_baz_misc(self):
a = Artifact.import_data('C1[Baz]', 'element 1', view_type=str)
b = Artifact.import_data('C3[Foo, Bar, Baz]',
'element 2', view_type=str)
x, y = self.run_action(a=a, b=b)
self.assertEqual(repr(x.type), 'C2[Foo, Foo]')
self.assertEqual(repr(y.type), 'Baz')
def test_mismatch(self):
a = Artifact.import_data('Bar', 'element 1', view_type=str)
b = Artifact.import_data('C3[Foo, Foo, Foo]',
'element 2', view_type=str)
with self.assertRaises(TypeError):
self.run_action(a=a, b=b)
class TestDoubleBoundVariableMethod(ActionTester):
ACTION = 'double_bound_variable_method'
def test_predicate_on_second(self):
a = Artifact.import_data('Bar', 'element 1', view_type=str)
b = Artifact.import_data('Bar % Properties("A")',
'element 2', view_type=str)
extra = Artifact.import_data('Foo', 'always foo', view_type=str)
x, = self.run_action(a=a, b=b, extra=extra)
self.assertEqual(repr(x.type), 'Baz')
def test_mismatch(self):
a = Artifact.import_data('Foo', 'element 1', view_type=str)
b = Artifact.import_data('Bar', 'element 2', view_type=str)
extra = Artifact.import_data('Foo', 'always foo', view_type=str)
with self.assertRaisesRegex(ValueError, 'match.*same output'):
self.run_action(a=a, b=b, extra=extra)
class TestBoolFlagSwapsOutputMethod(ActionTester):
ACTION = 'bool_flag_swaps_output_method'
def test_true(self):
a = Artifact.import_data('Bar', 'element', view_type=str)
x, = self.run_action(a=a, b=True)
self.assertEqual(repr(x.type), 'C1[Foo]')
def test_false(self):
a = Artifact.import_data('Bar', 'element', view_type=str)
x, = self.run_action(a=a, b=False)
self.assertEqual(repr(x.type), 'Foo')
class TestPredicatesPreservedMethod(ActionTester):
ACTION = 'predicates_preserved_method'
def test_simple(self):
a = Artifact.import_data("Foo % Properties('A')",
'element 1', view_type=str)
x, = self.run_action(a=a)
self.assertEqual(repr(x.type), "Foo % Properties('A')")
def test_mismatch(self):
a = Artifact.import_data("Foo % Properties('X')",
'element 1', view_type=str)
with self.assertRaises(TypeError):
self.run_action(a=a)
def test_combinations_preserved(self):
a = Artifact.import_data("Foo % Properties('A', 'B')",
'element 1', view_type=str)
x, = self.run_action(a=a)
self.assertEqual(repr(x.type), "Foo % Properties('A', 'B')")
def test_extra_dropped(self):
a = Artifact.import_data("Foo % Properties('Extra', 'A', 'B')",
'element 1', view_type=str)
x, = self.run_action(a=a)
self.assertEqual(repr(x.type), "Foo % Properties('A', 'B')")
if __name__ == '__main__':
unittest.main()
......@@ -17,7 +17,8 @@ from .format import (
SingleIntFormat,
MappingFormat,
UnimportableFormat,
RedundantSingleIntDirectoryFormat
RedundantSingleIntDirectoryFormat,
EchoFormat
)
from .plugin import dummy_plugin, citations
......@@ -167,3 +168,11 @@ def _1(data: list) -> FourIntsDirectoryFormat:
@dummy_plugin.register_transformer
def _4(ff: UnimportableFormat) -> int:
return 1
@dummy_plugin.register_transformer
def _a1(data: str) -> EchoFormat:
ff = EchoFormat()
with ff.open() as fh:
fh.write(data)
return ff
......@@ -6,15 +6,36 @@
# The full license is in the file LICENSE, distributed with this software.
# ----------------------------------------------------------------------------
import qiime2.plugin
import qiime2.plugin as plugin
IntSequence1 = qiime2.plugin.SemanticType('IntSequence1')
IntSequence2 = qiime2.plugin.SemanticType('IntSequence2')
Mapping = qiime2.plugin.SemanticType('Mapping')
FourInts = qiime2.plugin.SemanticType('FourInts')
SingleInt = qiime2.plugin.SemanticType('SingleInt')
IntSequence1 = plugin.SemanticType('IntSequence1')
IntSequence2 = plugin.SemanticType('IntSequence2')
Mapping = plugin.SemanticType('Mapping')
FourInts = plugin.SemanticType('FourInts')
SingleInt = plugin.SemanticType('SingleInt')
Kennel = qiime2.plugin.SemanticType('Kennel', field_names='pet')
Dog = qiime2.plugin.SemanticType('Dog', variant_of=Kennel.field['pet'])
Cat = qiime2.plugin.SemanticType('Cat', variant_of=Kennel.field['pet'])
Kennel = plugin.SemanticType('Kennel', field_names='pet')
Dog = plugin.SemanticType('Dog', variant_of=Kennel.field['pet'])
Cat = plugin.SemanticType('Cat', variant_of=Kennel.field['pet'])
# Kennel[Dog | Cat]
C1 = plugin.SemanticType('C1', field_names='first')
C2 = plugin.SemanticType('C2', field_names=['first', 'second'],
variant_of=C1.field['first'],
field_members={'first': [C1], 'second': [C1]})
C3 = plugin.SemanticType('C3', field_names=['first', 'second', 'third'],
variant_of=[C1.field['first'], C2.field['first'],
C2.field['second']],
field_members={'first': [C1, C2],
'second': [C1, C2],
'third': [C1, C2]})
_variants = [
C1.field['first'], C2.field['first'], C3.field['first'],
C2.field['second'], C3.field['second'],
C3.field['third']
]
Foo = plugin.SemanticType('Foo', variant_of=_variants)
Bar = plugin.SemanticType('Bar', variant_of=_variants)
Baz = plugin.SemanticType('Baz', variant_of=_variants)
# C1[C2[C3[Foo, Bar, Baz], C1[Foo]]] ... etc
......@@ -6,27 +6,34 @@
# The full license is in the file LICENSE, distributed with this software.
# ----------------------------------------------------------------------------
from .collection import List, Set, is_collection_type
from .semantic import SemanticType, is_semantic_type, Properties
from .primitive import (Str, Int, Float, Color, Metadata, Bool, MetadataColumn,
Categorical, Numeric, Range, Choices,
is_primitive_type)
from .collection import List, Set
from .semantic import SemanticType, Properties
from .primitive import (Str, Int, Float, Metadata, Bool, MetadataColumn,
Categorical, Numeric, Range, Start, End, Choices)
from .visualization import Visualization
from .signature import PipelineSignature, MethodSignature, VisualizerSignature
from .meta import TypeMap, TypeMatch
from .util import (is_primitive_type, is_semantic_type, is_metadata_type,
is_collection_type, is_visualization_type,
interrogate_collection_type, parse_primitive)
__all__ = [
# Type Helpers
'is_semantic_type', 'is_primitive_type', 'is_collection_type',
'is_semantic_type', 'is_visualization_type', 'is_primitive_type',
'is_metadata_type', 'is_collection_type', 'interrogate_collection_type',
'parse_primitive',
# Collection Types
'Set', 'List',
# Semantic Types
'SemanticType',
'Properties',
# Primitive Types
'Str', 'Int', 'Float', 'Bool', 'Color', 'Metadata', 'MetadataColumn',
'Categorical', 'Numeric', 'Range', 'Choices',
'Str', 'Int', 'Float', 'Bool', 'Metadata', 'MetadataColumn',
'Categorical', 'Numeric', 'Range', 'Start', 'End', 'Choices',
# Visualization Type
'Visualization',
# Signatures
'PipelineSignature', 'MethodSignature', 'VisualizerSignature'
'PipelineSignature', 'MethodSignature', 'VisualizerSignature',
# Variables
'TypeMap', 'TypeMatch'
]
......@@ -8,89 +8,49 @@
import json
from . import grammar
from .primitive import _PrimitiveBase, is_primitive_type, Metadata
from .semantic import _SemanticMixin, is_semantic_type
from qiime2.core.type.template import TypeTemplate
def is_collection_type(type_):
return isinstance(type_, (_CollectionBase, _CollectionExpression))
class _CollectionBase(TypeTemplate):
public_proxy = 'encode', 'decode'
def __init__(self):
# For semantic types
self.variant_of = frozenset()
class _CollectionBase(grammar.CompositeType):
def __init__(self, name, view):
# Only 1d collections are supported for now
self._view = view
super().__init__(name, field_names=['elements'])
def __eq__(self, other):
return type(self) is type(other)
def _apply_fields_(self, fields):
elements, = fields
if is_collection_type(elements):
# This must be the first branch, as is_primitive/semantic is true
# for collections types as well.
raise TypeError("Cannot nest collection types.")
elif is_semantic_type(elements):
return _CollectionSemantic(self.name, self._view, fields=fields)
elif is_primitive_type(elements):
# TODO consider making an `is_metadata_type` helper if this check
# is repeated in other parts of the codebase
if elements is Metadata or elements.name == 'MetadataColumn':
raise TypeError("Cannot use collections on metadata.")
return _CollectionPrimitive(self.name, self._view, fields=fields)
else:
raise NotImplementedError
def get_name(self):
return self.__class__.__name__[1:] # drop `_`
def get_kind_expr(self, self_expr):
if self_expr.fields:
return self_expr.fields[0].kind
return ""
class _CollectionExpression(grammar.TypeExpression):
def __init__(self, name, view, fields=(), predicate=None):
self._view = view
super().__init__(name, fields, predicate)
def get_kind(self):
raise NotImplementedError
def _is_element_(self, value):
contained_type, = self.fields
if not isinstance(value, self._view):
return False
if not value:
# Collections of size 1 are not allowed as it overlaps with
# optional values in an awkward way that is difficult to explain
# to end-users and make interfaces more difficult.
return False
for el in value:
if el not in contained_type:
def is_variant(self, self_expr, varfield):
return False
return True
def to_ast(self):
ast = super().to_ast()
ast['type'] = 'collection'
return ast
def _validate_union_(self, other, handshake=False):
# This is actually handled really well by the grammar, but interfaces
# would need to observe unions which may have multiple collections
# or mixes of collections and singletons, which is more complicated
# and we don't need it.
# If this is supported in the future, we should check that the type
# of self and other are the same so that we don't mix primitive and
# semantic types.
raise TypeError("Unions of collection types are not supported.")
def _validate_predicate_(self, predicate):
# Something like `MinLen(...)` might be handy...
raise TypeError("There are no predicates for collection types.")
def _apply_fields_(self, fields):
# Just pass the `view` along.
return self.__class__(self.name, self._view, fields=fields,
predicate=self.predicate)
def is_concrete(self):
# Prevent use as an output or artifact type
def validate_predicate(self, predicate):
raise TypeError("Predicates cannot be applied to %r" % self.get_name())
def is_element_expr(self, self_expr, value):
contained_expr = self_expr.fields[0]
if isinstance(value, self._view) and len(value) > 0:
return all(v in contained_expr for v in value)
return False
def is_element(self, value):
raise NotImplementedError
def get_union_membership_expr(self, self_expr):
return self.get_name() + '-' + self.get_kind_expr(self_expr)
class _CollectionPrimitive(_CollectionExpression, _PrimitiveBase):
# For primitive types
def encode(self, value):
return json.dumps(list(value))
......@@ -98,11 +58,43 @@ class _CollectionPrimitive(_CollectionExpression, _PrimitiveBase):
return self._view(json.loads(string))
class _CollectionSemantic(_CollectionExpression, _SemanticMixin):
def is_variant(self, varfield):
# Collections shouldn't be part of a composite type
return False
class _1DCollectionBase(_CollectionBase):
def validate_field(self, name, field):
if isinstance(field, _1DCollectionBase):
raise TypeError("Cannot nest collection types.")
if field.get_name() in {'MetadataColumn', 'Metadata'}:
raise TypeError("Cannot use %r with metadata." % self.get_name())
def get_field_names(self):
return ['type']
class _Set(_1DCollectionBase):
_view = set
class _List(_1DCollectionBase):
_view = list
class _Tuple(_CollectionBase):
_view = tuple
def get_kind_expr(self, self_expr):
return ""
def get_field_names(self):
return ['*types']
def validate_field_count(self, count):
if not count:
raise TypeError("Tuple type must contain at least one element.")
def validate_field(self, name, field):
# Tuples may contain anything, and as many fields as desired
pass
List = _CollectionBase('List', list)
Set = _CollectionBase('Set', set)
Set = _Set()
List = _List()
Tuple = _Tuple()
This diff is collapsed.
# ----------------------------------------------------------------------------
# Copyright (c) 2016-2019, QIIME 2 development team.
#
# Distributed under the terms of the Modified BSD License.
#
# The full license is in the file LICENSE, distributed with this software.
# ----------------------------------------------------------------------------
import itertools
from types import MappingProxyType
from ..util import superscript, tuplize, ImmutableBase
from .grammar import UnionExp, TypeExp
from .collection import Tuple
class TypeVarExp(UnionExp):
def __init__(self, members, tmap, input=False, output=False, index=None):
self.mapping = tmap
self.input = input
self.output = output
self.index = index
super().__init__(members)
def __repr__(self):
numbers = {}
for idx, m in enumerate(self.members, 1):
if m in numbers:
numbers[m] += superscript(',' + str(idx))
else:
numbers[m] = superscript(idx)
return " | ".join([repr(k) + v for k, v in numbers.items()])
def uniq_upto_sub(self, a_expr, b_expr):
"""
Two elements are unique up to a subtype if they are indistinguishable
with respect to that subtype. In the case of a type var, that means
the same branches must be "available" in the type map.
This means that A or B may have additional refinements (or may even be
subtypes of each other), so long as that does not change the branch
chosen by the type map.
"""
a_branches = [m for m in self.members if a_expr <= m]
b_branches = [m for m in self.members if b_expr <= m]
return a_branches == b_branches
def __eq__(self, other):
return (type(self) is type(other)
and self.index == other.index
and self.mapping == other.mapping)
def __hash__(self):
return hash(self.index) ^ hash(self.mapping)
def is_concrete(self):
return False
def can_intersect(self):
return False
def get_union_membership_expr(self, self_expr):
return None
def _is_subtype_(self, other):
return all(m <= other for m in self.members)
def _is_supertype_(self, other):
return any(m >= other for m in self.members)
def __iter__(self):
yield from self.members
def unpack_union(self):
yield self
def to_ast(self):
return {
"type": "variable",
"index": self.index,
"group": id(self.mapping),
"outputs": self.mapping.input_width(),
"mapping": [
([k.to_ast() for k in key.fields]
+ [v.to_ast() for v in value.fields])
for key, value in self.mapping.lifted.items()]
}
class TypeMap(ImmutableBase):
def __init__(self, mapping):
mapping = {Tuple[tuplize(k)]: Tuple[tuplize(v)]
for k, v in mapping.items()}
branches = list(mapping)
for i, a in enumerate(branches):
for j in range(i, len(branches)):
b = branches[j]
try:
intersection = a & b
except TypeError:
raise ValueError("Cannot place %r and %r in the same "
"type variable." % (a, b))
if (intersection.is_bottom()
or intersection is a or intersection is b):
continue
for k in range(i):
if intersection <= branches[k]:
break
else:
raise ValueError(
"Ambiguous resolution for invocations with type %r."
" Could match %r or %r, add a new branch ABOVE these"
" two (or modify these branches) to correct this."
% (intersection.fields, a.fields, b.fields))
self.__lifted = mapping
super()._freeze_()
@property
def lifted(self):
return MappingProxyType(self.__lifted)
def __eq__(self, other):
return self is other
def __hash__(self):
return hash(id(self))
def __iter__(self):
for idx, members in enumerate(
zip(*(k.fields for k in self.lifted.keys()))):
yield TypeVarExp(members, self, input=True, index=idx)
yield from self.iter_outputs()
def solve(self, *inputs):
inputs = Tuple[inputs]
for branch, outputs in self.lifted.items():
if inputs <= branch:
return outputs.fields
def input_width(self):
return len(next(iter(self.lifted.keys())).fields)
def iter_outputs(self, *, _double_as_input=False):
start = self.input_width()
for idx, members in enumerate(
zip(*(v.fields for v in self.lifted.values())), start):
yield TypeVarExp(members, self, output=True, index=idx,
input=_double_as_input)
def _get_intersections(listing):
intersections = []
for a, b in itertools.combinations(listing, 2):
i = a & b
if i.is_bottom() or i is a or i is b:
continue
intersections.append(i)
return intersections
def TypeMatch(listing):
listing = list(listing)
intersections = _get_intersections(listing)
to_add = []
while intersections:
to_add.extend(intersections)
intersections = _get_intersections(intersections)
mapping = TypeMap({l: l for l in list(reversed(to_add)) + listing})
# TypeMatch only produces a single variable
# iter_outputs is used by match for solving, so the index must match
return next(iter(mapping.iter_outputs(_double_as_input=True)))
def select_variables(expr):
"""When called on an expression, will yield selectors to the variable.
A selector will either return the variable (or equivalent fragment) in
an expression, or will return an entirely new expression with the
fragment replaced with the value of `swap`.
e.g.
>>> select_u, select_t = select_variables(Example[T] % U)
>>> t = select_t(Example[T] % U)
>>> assert T is t
>>> u = select_u(Example[T] % U)
>>> assert U is u
>>> frag = select_t(Example[Foo] % Bar)
>>> assert frag is Foo
>>> new_expr = select_t(Example[T] % U, swap=frag)
>>> assert new_expr == Example[Foo] % U
"""
if type(expr) is TypeVarExp:
def select(x, swap=None):
if swap is not None:
return swap
return x
yield select
return
if type(expr) is not TypeExp:
return
if type(expr.full_predicate) is TypeVarExp:
def select(x, swap=None):
if swap is not None:
return x.duplicate(predicate=swap)
return x.full_predicate
yield select
for idx, field in enumerate(expr.fields):
for sel in select_variables(field):
# Without this closure, the idx in select will be the last
# value of the enumerate, same for sel
# (Same problem as JS with callbacks inside a loop)
def closure(idx, sel):
def select(x, swap=None):
if swap is not None:
new_fields = list(x.fields)
new_fields[idx] = sel(x.fields[idx], swap)
return x.duplicate(fields=tuple(new_fields))
return sel(x.fields[idx])
return select
yield closure(idx, sel)
def match(provided, inputs, outputs):
provided_binding = {}
error_map = {}
for key, expr in inputs.items():
for selector in select_variables(expr):
var = selector(expr)
provided_fragment = selector(provided[key])
try:
current_binding = provided_binding[var]
except KeyError:
provided_binding[var] = provided_fragment
error_map[var] = provided[key]
else:
if not var.uniq_upto_sub(current_binding, provided_fragment):
raise ValueError("Received %r and %r, but expected %r"
" and %r to match (or to select the same"
" output)."
% (error_map[var], provided[key],
current_binding, provided_fragment))
# provided_binding now maps TypeVarExp instances to a TypeExp instance
# which is the relevent fragment from the provided input types
grouped_maps = {}
for item in provided_binding.items():
var = item[0]
if var.mapping not in grouped_maps:
grouped_maps[var.mapping] = [item]
else:
grouped_maps[var.mapping].append(item)
# grouped_maps now maps a TypeMap instance to tuples of
# (TypeVarExp, TypeExp) which are the items of provided_binding
# i.e. all of the bindings are now grouped under their shared type maps
output_fragments = {}
for mapping, group in grouped_maps.items():
if len(group) != mapping.input_width():
raise ValueError("Missing input variables")
inputs = [x[1] for x in sorted(group, key=lambda x: x[0].index)]
solved = mapping.solve(*inputs)
if solved is None:
provided = tuple(error_map[x[0]]
for x in sorted(group, key=lambda x: x[0].index))
raise ValueError("No solution for inputs: %r, check the signature "
"to see valid combinations." % (provided,))
# type vars share identity by instance of map and index, so we will
# be able to see the "same" vars again when looking up the outputs
for var, out in zip(mapping.iter_outputs(), solved):
output_fragments[var] = out
# output_fragments now maps a TypeVarExp to a TypeExp which is the solved
# fragment for the given output type variable
results = {}
for key, expr in outputs.items():
r = expr # output may not have a typevar, so default is the expr
for selector in select_variables(expr):
var = selector(expr)
r = selector(r, swap=output_fragments[var])
results[key] = r
# results now maps a key to a full TypeExp as solved by the inputs
return results
# ----------------------------------------------------------------------------
# Copyright (c) 2016-2019, QIIME 2 development team.
#
# Distributed under the terms of the Modified BSD License.
#
# The full license is in the file LICENSE, distributed with this software.
# ----------------------------------------------------------------------------
import ast
from . import grammar, meta, collection, primitive, semantic, visualization
def string_to_ast(type_expr):
try:
parsed = ast.parse(type_expr)
except SyntaxError:
raise ValueError("%r could not be parsed, it may not be a QIIME 2 type"
" or it may not be an atomic type. Use"
" `ast_to_type` instead." % (type_expr,))
if type(parsed) is not ast.Module:
# I don't think this branch *can* be hit
raise ValueError("%r is not a type expression." % (type_expr,))
try:
expr, = parsed.body
except ValueError:
raise ValueError("Only one type expression may be parse at a time, got"
": %r" % (type_expr,))
return _expr(expr.value)
def _expr(expr):
node = type(expr)
if node is ast.Name:
return _build_atomic(expr.id)
if node is ast.Call:
args = _parse_args(expr.args)
kwargs = _parse_kwargs(expr.keywords)
return _build_predicate(expr.func.id, args, kwargs)
if node is ast.Subscript:
field_expr = expr.slice.value
if type(field_expr) is ast.Tuple:
field_expr = field_expr.elts
else:
field_expr = (field_expr,)
base = _expr(expr.value)
base['fields'] = [_expr(e) for e in field_expr]
return base
if node is ast.BinOp:
op = type(expr.op)
left = _expr(expr.left)
right = _expr(expr.right)
if op is ast.Mod:
left['predicate'] = right
return left
if op is ast.BitOr:
return _build_union(left, right)
if op is ast.BitAnd:
return _build_intersection(left, right)
raise ValueError("Unknown expression: %r" % node)
def _convert_literals(expr):
node = type(expr)
if node is ast.List:
return [_convert_literals(e) for e in expr.elts]
if node is ast.Set:
return {_convert_literals(e) for e in expr.elts}
if node is ast.Tuple:
return tuple(_convert_literals(e) for e in expr.elts)
if node is ast.Dict:
return {_convert_literals(k): _convert_literals(v)
for k, v in zip(expr.keys, expr.values)}
if node is ast.NameConstant:
return expr.value
if node is ast.Name and expr.id == 'inf':
return float('inf')
if node is ast.Num:
return expr.n
if node is ast.Str:
return expr.s
raise ValueError("Unknown literal: %r" % node)
def _parse_args(args):
return tuple(_convert_literals(e) for e in args)
def _parse_kwargs(kwargs):
return {e.arg: _convert_literals(e.value) for e in kwargs}
def _build_predicate(name, args, kwargs):
base = {
'type': 'predicate',
'name': name
}
if name == 'Properties':
return _build_properties(base, args, kwargs)
if name == 'Range':
return _build_range(base, args, kwargs)
if name == 'Choices':
return _build_choices(base, args, kwargs)
def _normalize_input_collection(args):
if len(args) == 1 and isinstance(args[0], (list, set, tuple)):
return tuple(args[0])
return args
def _build_choices(base, args, kwargs):
if 'choices' in kwargs:
args = (kwargs['choices'],)
args = _normalize_input_collection(args)
base['choices'] = list(args)
return base
def _build_range(base, args, kwargs):
inclusive_start = kwargs.get('inclusive_start', True)
inclusive_end = kwargs.get('inclusive_end', False)
start = None
end = None
if len(args) == 1:
end = args[0]
elif len(args) != 0:
start, end = args
if start == float('-inf'):
start = None
if end == float('inf'):
end = None
base['range'] = [start, end]
base['inclusive'] = [inclusive_start, inclusive_end]
return base
def _build_properties(base, args, kwargs):
exclude = kwargs.get('exclude', [])
if 'include' in kwargs:
args = (kwargs['include'],)
args = _normalize_input_collection(args)
base['include'] = list(args)
base['exclude'] = list(exclude)
return base
def _build_atomic(name):
return {
'type': 'expression',
'builtin': name in {'Str', 'Int', 'Float', 'Bool',
'List', 'Set', 'Tuple', 'Visualization',
'Metadata', 'MetadataColumn', 'Numeric',
'Categorical'},
'name': name,
'predicate': None,
'fields': []
}
def _build_union(left, right):
return _build_ident(left, right, 'union')
def _build_intersection(left, right):
return _build_ident(left, right, 'intersection')
def _build_ident(left, right, type):
members = []
if left['type'] == type:
members.extend(left['members'])
else:
members.append(left)
if right['type'] == type:
members.extend(right['members'])
else:
members.append(right)
return {
'type': type,
'members': members
}
def ast_to_type(json_ast, scope=None):
if scope is None:
scope = {}
type_ = json_ast['type']
if type_ == 'expression':
predicate = json_ast['predicate']
if predicate is not None:
predicate = ast_to_type(predicate, scope=scope)
fields = json_ast['fields']
if len(fields) > 0:
fields = [ast_to_type(f, scope=scope) for f in fields]
name = json_ast['name']
if not json_ast['builtin']:
base_template = semantic.SemanticType(name).template
elif name == 'Visualization':
return visualization.Visualization
elif name in {'List', 'Set', 'Tuple'}:
base_template = getattr(collection, name).template
else:
base_template = getattr(primitive, name).template
return grammar.TypeExp(base_template,
fields=fields, predicate=predicate)
if type_ == 'predicate':
name = json_ast['name']
if name == 'Choices':
return primitive.Choices(json_ast['choices'])
if name == 'Range':
return primitive.Range(*json_ast['range'],
inclusive_start=json_ast['inclusive'][0],
inclusive_end=json_ast['inclusive'][1])
if name == 'Properties':
return semantic.Properties(json_ast['include'],
exclude=json_ast['exclude'])
if type_ == 'union':
members = [ast_to_type(m, scope=scope) for m in json_ast['members']]
return grammar.UnionExp(members)
if type_ == 'intersection':
members = [ast_to_type(m, scope=scope) for m in json_ast['members']]
return grammar.IntersectionExp(members)
if type_ == 'variable':
var_group = json_ast['group']
if var_group not in scope:
mapping = {}
out_idx = json_ast['outputs']
for entry in json_ast['mapping']:
entry = [ast_to_type(e) for e in entry]
mapping[tuple(entry[:out_idx])] = tuple(entry[out_idx:])
scope[var_group] = list(meta.TypeMap(mapping))
return scope[var_group][json_ast['index']]
......@@ -7,60 +7,28 @@
# ----------------------------------------------------------------------------
import numbers
import re
import itertools
from qiime2.core.type.grammar import (
TypeExpression, CompositeType, Predicate, UnionTypeExpression)
from qiime2.core.type.template import TypeTemplate, PredicateTemplate
import qiime2.metadata as metadata
import qiime2.core.util as util
def is_primitive_type(type_):
return isinstance(type_, _PrimitiveBase)
class _PrimitiveBase(TypeExpression):
def _validate_union_(self, other, handshake=False):
# It is possible we may want this someday: `Int | Str`, but order of
# encode/decode dispatch wouldn't be straight-forward.
# Order of the declaration could indicate "MRO", but then equality
# must consider Int | Str != Str | Int, which feels weird.
raise TypeError("Cannot union primitive types.")
def _validate_intersection_(self, other, handshake=False):
# This literally makes no sense for primitives. Even less sense than
# C's Union type (which is actually an intersection type...)
raise TypeError("Cannot intersect primitive types.")
class _Primitive(_PrimitiveBase):
def _validate_predicate_(self, predicate):
super()._validate_predicate_(predicate)
# _valid_predicates will be on the class obj of the primitives to
# make defining them easy
if not isinstance(predicate, tuple(self._valid_predicates)):
raise TypeError("Cannot supply %r as a predicate to %r type,"
" permitted predicates: %r"
% (predicate, self,
{p.__name__ for p in self._valid_predicates}))
for bound in predicate.iter_boundaries():
if not self._is_element_(bound):
raise TypeError("%r has the wrong types for %r."
% (predicate, self))
_RANGE_DEFAULT_START = float('-inf')
_RANGE_DEFAULT_END = float('inf')
_RANGE_DEFAULT_INCLUSIVE_START = True
_RANGE_DEFAULT_INCLUSIVE_END = False
def to_ast(self):
ast = super().to_ast()
ast['type'] = 'primitive'
return ast
class _PrimitivePredicateBase(PredicateTemplate):
def get_kind(self):
return 'primitive'
_RANGE_DEFAULT_START = None
_RANGE_DEFAULT_END = None
_RANGE_DEFAULT_INCLUSIVE_START = True
_RANGE_DEFAULT_INCLUSIVE_END = False
def get_name(self):
return self.__class__.__name__
class Range(Predicate):
class Range(_PrimitivePredicateBase):
def __init__(self, *args, inclusive_start=_RANGE_DEFAULT_INCLUSIVE_START,
inclusive_end=_RANGE_DEFAULT_INCLUSIVE_END):
if len(args) == 2:
......@@ -72,12 +40,17 @@ class Range(Predicate):
self.start = _RANGE_DEFAULT_START
self.end = _RANGE_DEFAULT_END
else:
raise ValueError("")
raise ValueError("Too many arguments passed, expected 0, 1, or 2.")
self.inclusive_start = inclusive_start
self.inclusive_end = inclusive_end
truthy = self.start is not None or self.end is not None
super().__init__(truthy)
if self.start is None:
self.start = _RANGE_DEFAULT_START
if self.end is None:
self.end = _RANGE_DEFAULT_END
if self.end < self.start:
raise ValueError("End of range precedes start.")
def __hash__(self):
return (hash(type(self)) ^
......@@ -95,24 +68,29 @@ class Range(Predicate):
def __repr__(self):
args = []
args.append(repr(self.start))
args.append(repr(self.end))
start = self.start
if start == float('-inf'):
start = None
end = self.end
if end == float('inf'):
end = None
args.append(repr(start))
args.append(repr(end))
if self.inclusive_start is not _RANGE_DEFAULT_INCLUSIVE_START:
args.append('inclusive_start=%r' % self.inclusive_start)
if self.inclusive_end is not _RANGE_DEFAULT_INCLUSIVE_END:
args.append('inclusive_end=%r' % self.inclusive_end)
return "%s(%s)" % (self.__class__.__name__, ', '.join(args))
return "Range(%s)" % (', '.join(args),)
def _is_element_(self, value):
if self.start is not None:
def is_element(self, value):
if self.inclusive_start:
if value < self.start:
return False
elif value <= self.start:
return False
if self.end is not None:
if self.inclusive_end:
if value > self.end:
return False
......@@ -121,66 +99,223 @@ class Range(Predicate):
return True
def is_symbol_subtype(self, other):
if type(self) is not type(other):
return False
if other.start > self.start:
return False
elif (other.start == self.start
and (not other.inclusive_start)
and self.inclusive_start):
return False
if other.end < self.end:
return False
elif (other.end == self.end
and (not other.inclusive_end)
and self.inclusive_end):
return False
return True
def is_symbol_supertype(self, other):
if type(self) is not type(other):
return False
if other.start < self.start:
return False
elif (other.start == self.start
and (not self.inclusive_start)
and other.inclusive_start):
return False
if other.end > self.end:
return False
elif (other.end == self.end
and (not self.inclusive_end)
and other.inclusive_end):
return False
return True
def collapse_intersection(self, other):
if type(self) is not type(other):
return None
if self.start < other.start:
new_start = other.start
new_inclusive_start = other.inclusive_start
elif other.start < self.start:
new_start = self.start
new_inclusive_start = self.inclusive_start
else:
new_start = self.start
new_inclusive_start = (
self.inclusive_start and other.inclusive_start)
if self.end > other.end:
new_end = other.end
new_inclusive_end = other.inclusive_end
elif other.end > self.end:
new_end = self.end
new_inclusive_end = self.inclusive_end
else:
new_end = self.end
new_inclusive_end = self.inclusive_end and other.inclusive_end
if new_end < new_start:
return None
if (new_start == new_end
and not (new_inclusive_start and new_inclusive_end)):
return None
return self.__class__(new_start, new_end,
inclusive_start=new_inclusive_start,
inclusive_end=new_inclusive_end).template
def iter_boundaries(self):
if self.start is not None:
if self.start != float('-inf'):
yield self.start
if self.end is not None:
if self.end != float('inf'):
yield self.end
def to_ast(self):
ast = super().to_ast()
ast['start'] = self.start
ast['end'] = self.end
ast['inclusive-start'] = self.inclusive_start
ast['inclusive-end'] = self.inclusive_end
return ast
def update_ast(self, ast):
start = self.start
if start == float('-inf'):
start = None
end = self.end
if end == float('inf'):
end = None
ast['range'] = [start, end]
ast['inclusive'] = [self.inclusive_start, self.inclusive_end]
class Choices(Predicate):
def __init__(self, choices):
def Start(start, inclusive=_RANGE_DEFAULT_INCLUSIVE_START):
return Range(start, _RANGE_DEFAULT_END, inclusive_start=inclusive)
def End(end, inclusive=_RANGE_DEFAULT_INCLUSIVE_END):
return Range(_RANGE_DEFAULT_START, end, inclusive_end=inclusive)
class Choices(_PrimitivePredicateBase):
def __init__(self, *choices):
if not choices:
raise ValueError("'Choices' cannot be instantiated with an empty"
" set.")
self.choices = set(choices)
# Backwards compatibility with old Choices({1, 2, 3}) syntax
if len(choices) == 1:
if not isinstance(choices[0], (bool, str)):
choices = choices[0]
super().__init__(choices)
self.choices = choices = tuple(choices)
if len(choices) != len(set(choices)):
raise ValueError("Duplicates found in choices: %r"
% util.find_duplicates(choices))
def __hash__(self):
return hash(type(self)) ^ hash(frozenset(self.choices))
def __eq__(self, other):
return type(self) == type(other) and self.choices == other.choices
return (type(self) == type(other)
and set(self.choices) == set(other.choices))
def __repr__(self):
return "%s({%s})" % (self.__class__.__name__,
repr(sorted(self.choices))[1:-1])
return "%s(%s)" % (self.__class__.__name__,
repr(list(self.choices))[1:-1])
def _is_element_(self, value):
def is_element(self, value):
return value in self.choices
def is_symbol_subtype(self, other):
if type(self) is not type(other):
return False
return set(self.choices) <= set(other.choices)
def is_symbol_supertype(self, other):
if type(self) is not type(other):
return False
return set(self.choices) >= set(other.choices)
def collapse_intersection(self, other):
if type(self) is not type(other):
return None
new_choices_set = set(self.choices) & set(other.choices)
if not new_choices_set:
return None
# order by appearance:
new_choices = []
for c in itertools.chain(self.choices, other.choices):
if c in new_choices_set:
new_choices.append(c)
new_choices_set.remove(c)
return self.__class__(new_choices).template
def iter_boundaries(self):
yield from self.choices
def to_ast(self):
ast = super().to_ast()
def update_ast(self, ast):
ast['choices'] = list(self.choices)
return ast
def unpack_union(self):
for c in self.choices:
yield self.__class__(c)
class _PrimitiveTemplateBase(TypeTemplate):
public_proxy = 'encode', 'decode'
def __eq__(self, other):
return type(self) is type(other)
def __hash__(self):
return hash(type(self))
def get_name(self):
return self.__class__.__name__[1:] # drop `_`
def get_kind(self):
return 'primitive'
def get_field_names(self):
return []
def validate_field(self, name, field):
raise NotImplementedError
def validate_predicate_expr(self, self_expr, predicate_expr):
predicate = predicate_expr.template
if type(predicate) not in self._valid_predicates:
raise TypeError(str(predicate_expr))
class _Int(_Primitive):
for bound in predicate.iter_boundaries():
if not self.is_element_expr(self_expr, bound):
raise TypeError(bound)
def validate_predicate(self, predicate):
raise NotImplementedError
class _Int(_PrimitiveTemplateBase):
_valid_predicates = {Range}
def _is_element_(self, value):
# Works with numpy just fine.
return isinstance(value, numbers.Integral)
def is_element(self, value):
return (value is not True and value is not False
and isinstance(value, numbers.Integral))
def _is_subtype_(self, other):
if (isinstance(other, type(Float)) and
self.predicate <= other.predicate):
def is_symbol_subtype(self, other):
if other.get_name() == 'Float':
return True
return super()._is_subtype_(other)
return super().is_symbol_subtype(other)
def decode(self, string):
return int(string)
......@@ -189,21 +324,31 @@ class _Int(_Primitive):
return str(value)
class _Str(_Primitive):
class _Str(_PrimitiveTemplateBase):
_valid_predicates = {Choices}
decode = encode = lambda self, arg: arg
def _is_element_(self, value):
# No reason for excluding bytes other than extreme prejudice.
def is_element(self, value):
return isinstance(value, str)
def decode(self, string):
return str(string)
class _Float(_Primitive):
def encode(self, value):
return str(value)
class _Float(_PrimitiveTemplateBase):
_valid_predicates = {Range}
def _is_element_(self, value):
def is_symbol_supertype(self, other):
if other.get_name() == 'Int':
return True
return super().is_symbol_supertype(other)
def is_element(self, value):
# Works with numpy just fine.
return isinstance(value, numbers.Real)
return (value is not True and value is not False
and isinstance(value, numbers.Real))
def decode(self, string):
return float(string)
......@@ -212,11 +357,17 @@ class _Float(_Primitive):
return str(value)
class _Bool(_Primitive):
_valid_predicates = set()
class _Bool(_PrimitiveTemplateBase):
_valid_predicates = {Choices}
def is_element(self, value):
return value is True or value is False
def _is_element_(self, value):
return isinstance(value, bool)
def validate_predicate(self, predicate):
if type(predicate) is Choices:
if set(predicate.iter_boundaries()) == {True, False}:
raise TypeError("Choices should be ommitted when "
"Choices(True, False).")
def decode(self, string):
if string not in ('false', 'true'):
......@@ -231,21 +382,15 @@ class _Bool(_Primitive):
return 'false'
class _Color(_Str):
def _is_element_(self, value):
# Regex from: http://stackoverflow.com/a/1636354/579416
return bool(re.search(r'^#(?:[0-9a-fA-F]{3}){1,2}$', value))
class _Metadata(_Primitive):
class _Metadata(_PrimitiveTemplateBase):
_valid_predicates = set()
def _is_element_(self, value):
def is_element(self, value):
return isinstance(value, metadata.Metadata)
def decode(self, metadata):
# This interface should have already retrieved this object.
if not self._is_element_(metadata):
if not self.is_element(metadata):
raise TypeError("`Metadata` must be provided by the interface"
" directly.")
return metadata
......@@ -253,67 +398,79 @@ class _Metadata(_Primitive):
def encode(self, value):
# TODO: Should this be the provenance representation? Does that affect
# decode?
return value
class _MetadataColumn(CompositeType):
def _validate_field_(self, name, value):
if not isinstance(value, (_MetadataColumnType,
_MetadataColumnTypeUnion)):
raise TypeError("Unsupported type in field: %r" % value)
class _MetadataColumn(_PrimitiveTemplateBase):
_valid_predicates = set()
def _apply_fields_(self, fields):
return _MetadataColumnExpression(self.name, fields=fields)
def is_element_expr(self, self_expr, value):
return value in self_expr.fields[0]
def is_element(self, value):
raise NotImplementedError
class _MetadataColumnExpression(_Primitive):
_valid_predicates = set()
def get_field_names(self):
return ["type"]
def _is_element_(self, value):
return value in self.fields[0]
def validate_field(self, name, field):
if field.get_name() not in ("Numeric", "Categorical"):
raise TypeError("Unsupported type in field: %r"
% (field.get_name(),))
def decode(self, metadata_column):
def decode(self, metadata):
# This interface should have already retrieved this object.
if metadata_column not in self:
raise TypeError("`MetadataColumn` must be provided by the "
"interface directly.")
return metadata_column
if not self.is_element(metadata):
raise TypeError("`Metadata` must be provided by the interface"
" directly.")
return metadata
def encode(self, value):
# TODO: Should this be the provenance representation? Does that affect
# decode?
return value
class _MetadataColumnType(_Primitive):
class _Categorical(_PrimitiveTemplateBase):
_valid_predicates = set()
def __init__(self, name, view, fields=(), predicate=None):
self._view = view
super().__init__(name, fields, predicate)
def _is_element_(self, value):
return isinstance(value, self._view)
def _validate_union_(self, other, handshake=False):
if not isinstance(other, self.__class__):
raise TypeError("Unsupported union: %r" % other)
def get_union_membership_expr(self, self_expr):
return 'metadata-column'
def _build_union_(self, members):
return _MetadataColumnTypeUnion(members)
def is_element(self, value):
return isinstance(value, metadata.CategoricalMetadataColumn)
class _MetadataColumnTypeUnion(UnionTypeExpression):
pass
class _Numeric(_PrimitiveTemplateBase):
_valid_predicates = set()
Bool = _Bool('Bool')
Int = _Int('Int')
Float = _Float('Float')
Str = _Str('Str')
Color = _Color('Color')
Metadata = _Metadata('Metadata')
MetadataColumn = _MetadataColumn('MetadataColumn', field_names=['type'])
Numeric = _MetadataColumnType('Numeric', metadata.NumericMetadataColumn)
Categorical = _MetadataColumnType('Categorical',
metadata.CategoricalMetadataColumn)
def get_union_membership_expr(self, self_expr):
return 'metadata-column'
def is_element(self, value):
return isinstance(value, metadata.NumericMetadataColumn)
Int = _Int()
Float = _Float()
Bool = _Bool()
Str = _Str()
Metadata = _Metadata()
MetadataColumn = _MetadataColumn()
Categorical = _Categorical()
Numeric = _Numeric()
def infer_primitive_type(value):
for t in (Int, Float):
if value in t:
return t % Range(value, value, inclusive_end=True)
for t in (Bool, Str):
if value in t:
return t % Choices(value)
for t in (Metadata, MetadataColumn[Categorical], MetadataColumn[Numeric]):
if value in t:
return t
raise ValueError("Unknown primitive type: %r" % (value,))
......@@ -10,8 +10,9 @@ import types
import collections
import itertools
from . import grammar
from qiime2.core.util import overrides
from qiime2.core.type.grammar import IncompleteExp, UnionExp, IntersectionExp
from qiime2.core.type.template import TypeTemplate, PredicateTemplate
from qiime2.core.type.util import is_semantic_type, is_qiime_type
_RESERVED_NAMES = {
# Predicates:
......@@ -23,6 +24,7 @@ _RESERVED_NAMES = {
'numericmetacol', 'metadatacategory', 'float', 'double', 'number', 'set',
'list', 'bag', 'multiset', 'map', 'dict', 'nominal', 'ordinal',
'categorical', 'numeric', 'interval', 'ratio', 'continuous', 'discrete',
'tuple', 'row', 'record',
# Type System:
'semantictype', 'propertymap', 'propertiesmap', 'typemap', 'typevariable',
'predicate'
......@@ -43,13 +45,13 @@ def SemanticType(name, field_names=None, field_members=None, variant_of=None):
Parameters
----------
name : str
The name of the semantic type, this should match the variable which it
is assigned to.
The name of the semantic type: this should match the variable to which
the semantic type is assigned.
field_names : str, iterable of str, optional
Name(s) of the fields where member types and can be placed. This makes
the type a composite type, meaning that fields must be provided to be
a realized semantic type. These names will define an ad-hoc variant
types accessible as `name`.field[`field_names` member].
Name(s) of the fields where member types can be placed. This makes
the type a composite type, meaning that fields must be provided to
produce realized semantic types. These names will define ad-hoc
variant types accessible as `name`.field[`field_names` member].
field_members : mapping, optional
A mapping of strings in `field_names` to one or more semantic types
which are known to be members of the field (the variant type).
......@@ -67,14 +69,10 @@ def SemanticType(name, field_names=None, field_members=None, variant_of=None):
"""
_validate_name(name)
variant_of = _munge_variant_of(variant_of)
if field_names is not None or field_members is not None:
field_names = _munge_field_names(field_names)
field_members = _munge_field_members(field_names, field_members)
return _IncompleteSemanticType(name, field_names, field_members,
variant_of)
return _SemanticType(name, variant_of)
return SemanticTemplate(name, field_names, field_members, variant_of)
def _munge_variant_of(variant_of):
......@@ -93,9 +91,12 @@ def _munge_variant_of(variant_of):
def _munge_field_names(field_names):
if field_names is None:
return ()
if type(field_names) is str:
field_names = (field_names,)
else:
return (field_names,)
field_names = tuple(field_names)
for field_name in field_names:
if type(field_name) is not str:
......@@ -108,6 +109,9 @@ def _munge_field_names(field_names):
def _munge_field_members(field_names, field_members):
if field_names is None:
return {}
fixed = {k: () for k in field_names}
if field_members is None:
......@@ -121,7 +125,7 @@ def _munge_field_members(field_names, field_members):
if key not in field_names:
raise ValueError("Field member key: %r is not in `field_names`"
" (%r)." % (key, field_names))
if is_semantic_type(value):
if is_qiime_type(value) and is_semantic_type(value):
fixed[key] = (value,)
else:
value = tuple(value)
......@@ -133,10 +137,6 @@ def _munge_field_members(field_names, field_members):
return fixed
def is_semantic_type(type_):
return isinstance(type_, (_SemanticMixin, _IncompleteSemanticType))
class VariantField:
def __init__(self, type_name, field_name, field_members):
self.type_name = type_name
......@@ -145,9 +145,9 @@ class VariantField:
def is_member(self, semantic_type):
for field_member in self.field_members:
if isinstance(field_member, _IncompleteSemanticType):
if isinstance(field_member, IncompleteExp):
# Pseudo-subtyping like Foo[X] <= Foo[Any].
# (_IncompleteSemanticType will never have __le__ because you
# (IncompleteExp will never have __le__ because you
# are probably doing something wrong with it (this totally
# doesn't count!))
if semantic_type.name == field_member.name:
......@@ -165,164 +165,157 @@ class VariantField:
return "%s.field[%r]" % (self.type_name, self.field_name)
class _IncompleteSemanticType(grammar.CompositeType):
class SemanticTemplate(TypeTemplate):
public_proxy = 'field',
def __init__(self, name, field_names, field_members, variant_of):
self.name = name
self.field_names = field_names
self.field = types.MappingProxyType({
f: VariantField(name, f, field_members[f])
for f in self.field_names})
self.__field = {f: VariantField(name, f, field_members[f])
for f in self.field_names}
self.variant_of = variant_of
super().__init__(name, field_names)
@overrides(grammar.CompositeType)
def _validate_field_(self, name, value):
super()._validate_field_(name, value)
varfield = self.field[name]
if not value.is_variant(varfield):
raise TypeError("%r is not a variant of %r." % (value, varfield))
@overrides(grammar.CompositeType)
def _apply_fields_(self, fields):
return _SemanticType(self.name, self.variant_of, fields=fields)
@property
def field(self):
return types.MappingProxyType(self.__field)
def __eq__(self, other):
return (type(self) is type(other)
and self.name == other.name
and self.fields == other.fields
and self.variant_of == other.variant_of)
class _SemanticMixin:
def is_variant(self, varfield):
return varfield in self.variant_of or varfield.is_member(self)
def __hash__(self):
return (hash(type(self)) ^ hash(self.name)
^ hash(self.fields) ^ hash(self.variant_of))
def get_kind(self):
return 'semantic-type'
class _SemanticType(grammar.TypeExpression, _SemanticMixin):
def __init__(self, name, variant_of, **kwargs):
self.variant_of = variant_of
def get_name(self):
return self.name
super().__init__(name, **kwargs)
def get_field_names(self):
return self.field_names
def _apply_fields_(self, fields):
return self.__class__(self.name, self.variant_of, fields=fields,
predicate=self.predicate)
def is_element_expr(self, self_expr, value):
import qiime2.sdk
if not isinstance(value, qiime2.sdk.Artifact):
return False
return value.type <= self_expr
def _validate_intersection_(self, other, handshake=False):
raise TypeError("Cannot intersect %r and %r. (Only semantic type"
" variables can be intersected.)" % (self, other))
def is_element(self, value):
raise NotImplementedError
def _build_union_(self, members):
return _SemanticUnionType(members)
def validate_field(self, name, field):
raise NotImplementedError
def _validate_predicate_(self, predicate):
def validate_fields_expr(self, self_expr, fields_expr):
self.validate_field_count(len(fields_expr))
for expr, varf in zip(fields_expr,
[self.field[n] for n in self.field_names]):
if (expr.template is not None
and hasattr(expr.template, 'is_variant')):
check = expr.template.is_variant
else:
check = self.is_variant
if not check(expr, varf):
raise TypeError("%r is not a variant of %r" % (expr, varf))
@classmethod
def is_variant(cls, expr, varf):
if isinstance(expr, UnionExp):
return all(cls.is_variant(e, varf) for e in expr.members)
if isinstance(expr, IntersectionExp):
return any(cls.is_variant(e, varf) for e in expr.members)
return varf.is_member(expr) or varf in expr.template.variant_of
def validate_predicate(self, predicate):
if not isinstance(predicate, Properties):
raise TypeError()
def _apply_predicate_(self, predicate):
return self.__class__(self.name, self.variant_of,
fields=self.fields, predicate=predicate)
def update_ast(self, ast):
ast['builtin'] = False
def _is_element_(self, value):
import qiime2.sdk
if not isinstance(value, qiime2.sdk.Artifact):
return False
return value.type <= self
def to_ast(self):
ast = super().to_ast()
ast['type'] = 'semantic-type'
ast['concrete'] = self.is_concrete()
return ast
class Properties(PredicateTemplate):
def __init__(self, *include, exclude=()):
if len(include) == 1 and isinstance(include[0],
(list, tuple, set, frozenset)):
include = tuple(include[0])
class _SemanticUnionType(grammar.UnionTypeExpression, _SemanticMixin):
@overrides(_SemanticMixin)
def is_variant(self, varfield):
return all(m.is_variant(varfield) for m in self)
class Properties(grammar.Predicate):
def __init__(self, include=(), exclude=()):
if type(include) is str:
include = (include,)
if type(exclude) is str:
exclude = (exclude,)
self.include = list(include)
self.exclude = list(exclude)
self.include = tuple(include)
self.exclude = tuple(exclude)
for prop in itertools.chain(self.include, self.exclude):
if type(prop) is not str:
raise TypeError("%r in %r is not a string." % (prop, self))
super().__init__(include, exclude)
def __hash__(self):
return hash(tuple(self.include)) ^ hash(tuple(self.exclude))
return hash(frozenset(self.include)) ^ hash(frozenset(self.exclude))
def __eq__(self, other):
return (type(self) is type(other) and
self.include == other.include and
self.exclude == other.exclude)
set(self.include) == set(other.include) and
set(self.exclude) == set(other.exclude))
def __repr__(self):
args = []
if self.include:
args.append("%r" % self.include)
args.append(', '.join(repr(s) for s in self.include))
if self.exclude:
args.append("exclude=%r" % self.exclude)
args.append("exclude=%r" % list(self.exclude))
return "%s(%s)" % (self.__class__.__name__, ', '.join(args))
def _is_subtype_(self, other):
if other is None:
return True
if type(other) != type(self):
# For PropertyMap or other complex comparisons
return NotImplemented
def is_symbol_subtype(self, other):
if type(self) is not type(other):
return False
return (set(other.include) <= set(self.include) and
set(other.exclude) <= set(self.exclude))
def to_ast(self):
ast = super().to_ast()
ast['include'] = self.include
ast['exclude'] = self.exclude
return ast
def is_symbol_supertype(self, other):
if type(self) is not type(other):
return False
return (set(other.include) >= set(self.include) and
set(other.exclude) >= set(self.exclude))
# TODO: Implement these classes:
class _SemanticIntersectionType(grammar.IntersectionTypeExpression,
_SemanticMixin):
def __init__(self):
# IMPORTANT! Remove this __init__ method.
raise NotImplementedError("TODO")
def collapse_intersection(self, other):
if type(self) is not type(other):
return None
@overrides(_SemanticMixin)
def is_variant(self, varfield):
return all(m.is_variant(varfield) for m in self)
new_include_set = set(self.include) | set(other.include)
new_exclude_set = set(self.exclude) | set(other.exclude)
new_include = []
new_exclude = []
for inc in itertools.chain(self.include, other.include):
if inc in new_include_set:
new_include.append(inc)
new_include_set.remove(inc)
for exc in itertools.chain(self.exclude, other.exclude):
if exc in new_exclude_set:
new_exclude.append(exc)
new_exclude_set.remove(exc)
class SemanticMap(grammar.MappingTypeExpression, _SemanticMixin):
def __init__(self):
# IMPORTANT! Remove this __init__ method.
raise NotImplementedError("TODO")
return self.__class__(*new_include, exclude=new_exclude).template
@overrides(_SemanticMixin)
def is_variant(self, varfield):
# A TypeMap may be on either side of the signature, we only know
# which side once it is placed in a signature. Otherwise, either
# construction is completely valid as long as all members agree.
return (all(m.is_variant(varfield) for m in self.mapping) or
all(m.is_variant(varfield) for m in self.mapping.values()))
def get_kind(self):
return 'semantic-type'
def _validate_member_(self, member):
if not member.is_concrete():
raise ValueError("")
def get_name(self):
return self.__class__.__name__
def _build_intersection_(self, members):
return _SemanticIntersectionType(members)
def is_element(self, expr):
return True # attached TypeExp checks this
def get_union_membership_expr(self, self_expr):
return 'predicate-' + self.get_name()
class PropertyMap(Properties):
def __init__(self, name, mapping):
raise NotImplementedError("TODO")
self.name = name
self.mapping = mapping
def __repr__(self):
return self.name
def update_ast(self, ast):
ast['include'] = list(self.include)
ast['exclude'] = list(self.exclude)
......@@ -12,10 +12,13 @@ import copy
import itertools
import qiime2.sdk
from .grammar import TypeExpression
from .primitive import is_primitive_type
from .semantic import is_semantic_type
from .grammar import TypeExp, UnionExp
from .meta import TypeVarExp
from .collection import List, Set
from .primitive import infer_primitive_type
from .visualization import Visualization
from . import meta
from .util import is_semantic_type, is_primitive_type
from ..util import ImmutableBase
......@@ -54,6 +57,16 @@ class ParameterSpec(ImmutableBase):
def has_description(self):
return self.description is not self.NOVALUE
def duplicate(self, **kwargs):
qiime_type = kwargs.pop('qiime_type', self.qiime_type)
view_type = kwargs.pop('view_type', self.view_type)
default = kwargs.pop('default', self.default)
description = kwargs.pop('description', self.description)
if kwargs:
raise TypeError("Unknown arguments: %r" % kwargs)
return ParameterSpec(qiime_type, view_type, default, description)
def __repr__(self):
return ("ParameterSpec(qiime_type=%r, view_type=%r, default=%r, "
"description=%r)" % (self.qiime_type, self.view_type,
......@@ -219,7 +232,7 @@ class PipelineSignature:
"Input %r must be a semantic QIIME type, not %r"
% (input_name, spec.qiime_type))
if not isinstance(spec.qiime_type, TypeExpression):
if not isinstance(spec.qiime_type, (TypeExp, UnionExp)):
raise TypeError(
"Input %r must be a complete semantic type expression, "
"not %r" % (input_name, spec.qiime_type))
......@@ -230,6 +243,13 @@ class PipelineSignature:
"value of `None` is supported for inputs."
% (input_name, spec.default))
for var_selector in meta.select_variables(spec.qiime_type):
var = var_selector(spec.qiime_type)
if not var.input:
raise TypeError("An output variable has been associated"
" with an input type: %r"
% spec.qiime_type)
def _assert_valid_parameters(self, parameters):
for param_name, spec in parameters.items():
if not is_primitive_type(spec.qiime_type):
......@@ -237,7 +257,7 @@ class PipelineSignature:
"Parameter %r must be a primitive QIIME type, not %r"
% (param_name, spec.qiime_type))
if not isinstance(spec.qiime_type, TypeExpression):
if not isinstance(spec.qiime_type, (TypeExp, UnionExp)):
raise TypeError(
"Parameter %r must be a complete primitive type "
"expression, not %r" % (param_name, spec.qiime_type))
......@@ -249,6 +269,13 @@ class PipelineSignature:
"semantic QIIME type %r or `None`."
% (param_name, spec.qiime_type))
for var_selector in meta.select_variables(spec.qiime_type):
var = var_selector(spec.qiime_type)
if not var.input:
raise TypeError("An output variable has been associated"
" with an input type: %r"
% spec.qiime_type)
def _assert_valid_outputs(self, outputs):
if len(outputs) == 0:
raise TypeError("%s requires at least one output"
......@@ -262,11 +289,17 @@ class PipelineSignature:
"Visualization, not %r"
% (output_name, spec.qiime_type))
if not isinstance(spec.qiime_type, TypeExpression):
if not isinstance(spec.qiime_type, (TypeVarExp, TypeExp)):
raise TypeError(
"Output %r must be a complete type expression, not %r"
% (output_name, spec.qiime_type))
for var_selector in meta.select_variables(spec.qiime_type):
var = var_selector(spec.qiime_type)
if not var.output:
raise TypeError("An input variable has been associated"
" with an input type: %r")
def _assert_valid_views(self, inputs, parameters, outputs):
for name, spec in itertools.chain(inputs.items(),
parameters.items(),
......@@ -289,24 +322,60 @@ class PipelineSignature:
def check_types(self, **kwargs):
for name, spec in self.signature_order.items():
if kwargs[name] not in spec.qiime_type:
parameter = kwargs[name]
# A type mismatch is unacceptable unless the value is None
# and this parameter's default value is None.
if not (spec.has_default() and
spec.default is None and
kwargs[name] is None):
if ((parameter not in spec.qiime_type) and
not (spec.has_default() and spec.default is None
and parameter is None)):
if isinstance(parameter, qiime2.sdk.Visualization):
raise TypeError(
"Parameter %r received a Visualization as an "
"argument. Visualizations may not be used as inputs."
% name)
elif isinstance(parameter, qiime2.sdk.Artifact):
raise TypeError(
"Parameter %r received an argument of type %r. An "
"argument of subtype %r is required." % (
name, kwargs[name].type, spec.qiime_type))
def solve_output(self, **input_types):
# TODO implement solving here. The check for concrete output types may
# be unnecessary here if the signature's constructor can detect
# unsolvable signatures and ensure that solving will always produce
# concrete output types.
"Parameter %r requires an argument of type %r. An "
"argument of type %r was passed." % (
name, spec.qiime_type, parameter.type))
elif isinstance(parameter, qiime2.Metadata):
raise TypeError(
"Parameter %r received Metadata as an "
"argument, which is incompatible with parameter "
"type: %r" % (name, spec.qiime_type))
else: # handle primitive types
raise TypeError(
"Parameter %r received %r as an argument, which is "
"incompatible with parameter type: %r"
% (name, parameter, spec.qiime_type))
def solve_output(self, **kwargs):
solved_outputs = None
for _, spec in itertools.chain(self.inputs.items(),
self.parameters.items(),
self.outputs.items()):
if list(meta.select_variables(spec.qiime_type)):
break # a variable exists, do the hard work
else:
# no variables
solved_outputs = self.outputs
if solved_outputs is None:
inputs = {**{k: s.qiime_type for k, s in self.inputs.items()},
**{k: s.qiime_type for k, s in self.parameters.items()}}
outputs = {k: s.qiime_type for k, s in self.outputs.items()}
input_types = {
k: self._infer_type(k, v) for k, v in kwargs.items()}
solved = meta.match(input_types, inputs, outputs)
solved_outputs = collections.OrderedDict(
(k, s.duplicate(qiime_type=solved[k]))
for k, s in self.outputs.items())
for output_name, spec in solved_outputs.items():
if not spec.qiime_type.is_concrete():
raise TypeError(
......@@ -315,6 +384,25 @@ class PipelineSignature:
return solved_outputs
def _infer_type(self, key, value):
if value is None:
if key in self.inputs:
return self.inputs[key].qiime_type
elif key in self.parameters:
return self.parameters[key].qiime_type
# Shouldn't happen:
raise ValueError("Parameter passed not consistent with signature.")
if type(value) is list:
inner = UnionExp((self._infer_type(v) for v in value))
return List[inner.normalize()]
if type(value) is set:
inner = UnionExp((self._infer_type(v) for v in value))
return Set[inner.normalize()]
if isinstance(value, qiime2.sdk.Artifact):
return value.type
else:
return infer_primitive_type(value)
def __repr__(self):
lines = []
for group in 'inputs', 'parameters', 'outputs':
......