Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
77 commits
Select commit Hold shift + click to select a range
5888a4f
add Taggable class
nchristensen Nov 20, 2020
6b5766c
flake8 fixes
nchristensen Nov 20, 2020
4a1965e
bump version
nchristensen Nov 20, 2020
00b4f76
Update pytools/tag.py
nchristensen Nov 24, 2020
418d4b8
Update pytools/tag.py
nchristensen Nov 24, 2020
f3d75d1
Update pytools/tag.py
nchristensen Nov 24, 2020
2c6cc06
add taggable function
nchristensen Nov 24, 2020
75c4fc0
add taggable function
nchristensen Nov 24, 2020
34bca92
add typing
nchristensen Nov 24, 2020
732b8f7
remove white space
nchristensen Nov 24, 2020
67dfcef
flake8 fixes
nchristensen Nov 24, 2020
7441bce
pylint fixes
nchristensen Nov 24, 2020
df00a22
add space
nchristensen Nov 24, 2020
a5f0ba5
pylint disable
nchristensen Nov 24, 2020
15442d2
mypy fix
nchristensen Nov 24, 2020
bd41585
mypy
nchristensen Nov 24, 2020
501f776
mypy
nchristensen Nov 24, 2020
394b45c
more pylint
nchristensen Nov 24, 2020
80e2c23
supress mypy
nchristensen Nov 24, 2020
ea1ec2e
use pipe syntax
nchristensen Nov 24, 2020
b1d9747
check uniqueness
nchristensen Nov 24, 2020
06cb808
flake8 fixes
nchristensen Nov 24, 2020
25befba
add without_tag method
nchristensen Nov 24, 2020
63496b7
flake8 fixes
nchristensen Nov 24, 2020
6a5169d
remove print statement
nchristensen Nov 24, 2020
baaf635
Remove comment
nchristensen Nov 24, 2020
3f65ca8
Update pytools/tag.py
nchristensen Nov 24, 2020
b3ba65a
Taggable changes
nchristensen Nov 24, 2020
745c7b7
Taggable fixes
nchristensen Nov 24, 2020
606947b
whitespaces
nchristensen Nov 24, 2020
618957f
use to_remove
nchristensen Nov 24, 2020
f25174e
use covariant self types on copy functions
nchristensen Nov 25, 2020
8f07ecd
flake8
nchristensen Nov 25, 2020
cab78f2
flake8
nchristensen Nov 25, 2020
22fce9f
bump down version number for now
nchristensen Nov 28, 2020
e392c02
bump up version number again
nchristensen Nov 28, 2020
576a222
handle None type
nchristensen Nov 28, 2020
d5ad701
flake8
nchristensen Nov 28, 2020
1a5ae24
allow None type
nchristensen Nov 28, 2020
aeb581f
allow kwargs on copy
nchristensen Nov 30, 2020
8697aab
add methods to docs
nchristensen Nov 30, 2020
daa573b
update copyright
nchristensen Nov 30, 2020
94c714e
just use kwargs
nchristensen Dec 1, 2020
68da5a1
_tags -> tag
nchristensen Dec 1, 2020
f2a28e1
remove commented part
nchristensen Dec 1, 2020
fa99492
blank line for sphinx
nchristensen Dec 1, 2020
1511358
input normalization function
nchristensen Dec 9, 2020
a7130c5
add comment and provide more useful error message
nchristensen Dec 11, 2020
4bb243c
remove print line
nchristensen Dec 11, 2020
497b52f
add test for Tag and Tagged
nchristensen Dec 11, 2020
eb9a448
Update pytools/version.py
nchristensen Dec 15, 2020
0e8fcbf
Merge branch 'master' into master
nchristensen Dec 15, 2020
f8f5e8e
fix typo
nchristensen Dec 15, 2020
3906209
ö
nchristensen Dec 15, 2020
b6ee450
bump versionadded
nchristensen Dec 15, 2020
b33a61a
Update pytools/tag.py
nchristensen Dec 18, 2020
07b25ce
Update pytools/tag.py
nchristensen Dec 18, 2020
8586773
replace input normalization with assertions in constructor
nchristensen Dec 18, 2020
0ff1382
replace O(n^2) algorithm
nchristensen Dec 22, 2020
1f94b14
lowercase variable names
nchristensen Dec 22, 2020
3ce7e4e
more lowercase variables
nchristensen Dec 22, 2020
7d48bf9
Propose tag uniqueness check algorithm
inducer Dec 22, 2020
883e4c0
Merge pull request #1 from inducer/unique-tag-check
nchristensen Dec 25, 2020
d88292e
update tag set
nchristensen Dec 25, 2020
84976b2
document meaning of UniqueTag
nchristensen Jan 4, 2021
9de36cb
Add test for multiple UniqueTag instances of different subclasses
nchristensen Jan 4, 2021
ac75212
Unique tag wording
nchristensen Jan 5, 2021
7439758
improve variable names
nchristensen Jan 5, 2021
c0c6a57
add test for multiple non-unique tags of the same type
nchristensen Jan 5, 2021
ccbae99
subclass ValueError
nchristensen Jan 5, 2021
cf67f1a
simplify tests
nchristensen Jan 5, 2021
453ee22
change test to ribbon example
nchristensen Jan 8, 2021
401d895
extra line
nchristensen Jan 8, 2021
c04202d
Rename _normalize_input -> _normalize_tags
inducer Jan 9, 2021
0f28d1d
Bump version
inducer Jan 9, 2021
daaaf9e
Bump version for Taggable to 2021.1
inducer Jan 9, 2021
82da2e0
Add missing role markup in UniqueTag docstring
inducer Jan 9, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
141 changes: 134 additions & 7 deletions pytools/tag.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
from dataclasses import dataclass
from typing import Tuple, Any, FrozenSet
from typing import Tuple, Any, FrozenSet, Union, Iterable, TypeVar
from pytools import memoize

__copyright__ = """
Copyright (C) 2020 Andreas Kloeckner
Copyright (C) 2020 Andreas Klöckner
Copyright (C) 2020 Matt Wala
Copyright (C) 2020 Xiaoyu Wei
Copyright (C) 2020 Nicholas Christensen
"""

__license__ = """
Expand All @@ -27,28 +29,30 @@
THE SOFTWARE.
"""


# {{{ docs

__doc__ = """

Tag Interface
---------------

.. autoclass:: Taggable
.. autoclass:: Tag
.. autoclass:: UniqueTag

Supporting Functionality
------------------------

.. autoclass:: DottedName
.. autoclass:: NonUniqueTagError

"""

# }}}

# }}}

# {{{ dotted name

# {{{ dotted name

class DottedName:
"""
Expand Down Expand Up @@ -89,6 +93,7 @@ def from_class(cls, argcls: Any) -> "DottedName":

# }}}


# {{{ tag

tag_dataclass = dataclass(init=True, eq=True, frozen=True, repr=True)
Expand Down Expand Up @@ -117,16 +122,138 @@ class Tag:
def tag_name(self) -> DottedName:
return DottedName.from_class(type(self))

# }}}


# {{{ unique tag

class UniqueTag(Tag):
"""
Only one instance of this type of tag may be assigned
to a single tagged object.
A superclass for tags that are unique on each :class:`Taggable`.

Each instance of :class:`Taggable` may have no more than one
instance of each subclass of :class:`UniqueTag` in its
set of `tags`. Multiple `UniqueTag` instances of
different (immediate) subclasses are allowed.
"""
pass

# }}}


TagsType = FrozenSet[Tag]
TagOrIterableType = Union[Iterable[Tag], Tag, None]
T_co = TypeVar("T_co", bound="Taggable")


# {{{ taggable

@memoize
def _immediate_unique_tag_descendants(cls):
if UniqueTag in cls.__bases__:
return frozenset([cls])
else:
result = frozenset()
for base in cls.__bases__:
result = result | _immediate_unique_tag_descendants(base)
return result


class NonUniqueTagError(ValueError):
"""
Raised when a :class:`Taggable` object is instantiated with more
than one :class:`UniqueTag` instances of the same subclass in
its set of tags.
"""
pass


class Taggable:
"""
Parent class for objects with a `tags` attribute.

.. attribute:: tags

A :class:`frozenset` of :class:`Tag` instances
Comment thread
nchristensen marked this conversation as resolved.

.. method:: copy
.. method:: tagged
.. method:: without_tags

.. versionadded:: 2021.1
"""

def __init__(self, tags: TagsType = frozenset()):
# For performance we assert rather than
# normalize the input.
assert isinstance(tags, FrozenSet)
assert all(isinstance(tag, Tag) for tag in tags)
self.tags = tags
self._check_uniqueness()

def _normalize_tags(self, tags: TagOrIterableType) -> TagsType:
if isinstance(tags, Tag):
t = frozenset([tags])
elif tags is None:
t = frozenset()
else:
t = frozenset(tags)
return t

def _check_uniqueness(self):
unique_tag_descendants = set()
for tag in self.tags:
tag_unique_tag_descendants = _immediate_unique_tag_descendants(
type(tag))
intersection = unique_tag_descendants & tag_unique_tag_descendants
if intersection:
raise NonUniqueTagError("Multiple tags are direct subclasses of "
"the following UniqueTag(s): "
f"{', '.join(d.__name__ for d in intersection)}")
else:
unique_tag_descendants.update(tag_unique_tag_descendants)

def copy(self: T_co, **kwargs: Any) -> T_co:
"""
Returns of copy of *self* with the specified tags. This method
should be overridden by subclasses.
"""
raise NotImplementedError("The copy function is not implemented.")

def tagged(self: T_co, tags: TagOrIterableType) -> T_co:
"""
Return a copy of *self* with the specified
tag or tags unioned. If *tags* is a :class:`pytools.tag.UniqueTag`
and other tags of this type are already present, an error is raised
Assumes `self.copy(tags=<NEW VALUE>)` is implemented.

:arg tags: An instance of :class:`Tag` or
an iterable with instances therein.
"""
new_tags = self._normalize_tags(tags)
union_tags = self.tags | new_tags
cpy = self.copy(tags=union_tags)
return cpy

def without_tags(self: T_co,
tags: TagOrIterableType, verify_existence: bool = True) -> T_co:
"""
Return a copy of *self* without the specified tags.
`self.copy(tags=<NEW VALUE>)` is implemented.

:arg tags: An instance of :class:`Tag` or an iterable with instances
therein.
:arg verify_existence: If set
to `True`, this method raises an exception if not all tags specified
for removal are present in the original set of tags. Default `True`
"""

to_remove = self._normalize_tags(tags)
new_tags = self.tags - to_remove
if verify_existence and len(new_tags) > len(self.tags) - len(to_remove):
raise ValueError("A tag specified for removal was not present.")

return self.copy(tags=new_tags)
Comment thread
nchristensen marked this conversation as resolved.

# }}}

Expand Down
2 changes: 1 addition & 1 deletion pytools/version.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
VERSION = (2020, 4, 4)
VERSION = (2021, 1)
VERSION_STATUS = ""
VERSION_TEXT = ".".join(str(x) for x in VERSION) + VERSION_STATUS
87 changes: 87 additions & 0 deletions test/test_pytools.py
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,93 @@ def test_make_obj_array_iteration():
# }}}


def test_tag():
from pytools.tag import Taggable, Tag, UniqueTag, NonUniqueTagError

# Need a subclass that defines the copy function in order to test.
class TaggableWithCopy(Taggable):

def copy(self, **kwargs):
return TaggableWithCopy(kwargs["tags"])

class FairRibbon(Tag):
pass

class BlueRibbon(FairRibbon):
pass

class RedRibbon(FairRibbon):
pass

class ShowRibbon(FairRibbon, UniqueTag):
pass

class BestInShowRibbon(ShowRibbon):
pass

class ReserveBestInShowRibbon(ShowRibbon):
pass

class BestInClassRibbon(FairRibbon, UniqueTag):
pass

best_in_show_ribbon = BestInShowRibbon()
reserve_best_in_show_ribbon = ReserveBestInShowRibbon()
blue_ribbon = BlueRibbon()
red_ribbon = RedRibbon()
best_in_class_ribbon = BestInClassRibbon()

# Test that instantiation fails if tags is not a FrozenSet of Tags
with pytest.raises(AssertionError):
TaggableWithCopy(tags=[best_in_show_ribbon, reserve_best_in_show_ribbon,
blue_ribbon, red_ribbon])

# Test that instantiation fails if tags is not a FrozenSet of Tags
with pytest.raises(AssertionError):
TaggableWithCopy(tags=frozenset((1, reserve_best_in_show_ribbon, blue_ribbon,
red_ribbon)))

# Test that instantiation fails if there are multiple instances
# of the same UniqueTag subclass
with pytest.raises(NonUniqueTagError):
TaggableWithCopy(tags=frozenset((best_in_show_ribbon,
reserve_best_in_show_ribbon, blue_ribbon, red_ribbon)))

# Test that instantiation succeeds if there are multiple instances
# Tag subclasses.
t1 = TaggableWithCopy(frozenset([reserve_best_in_show_ribbon, blue_ribbon,
red_ribbon]))
assert t1.tags == frozenset((reserve_best_in_show_ribbon, red_ribbon,
blue_ribbon))

# Test that instantiation succeeds if there are multiple instances
# of UniqueTag of different subclasses.
t1 = TaggableWithCopy(frozenset([reserve_best_in_show_ribbon,
best_in_class_ribbon, blue_ribbon,
blue_ribbon]))
assert t1.tags == frozenset((reserve_best_in_show_ribbon, best_in_class_ribbon,
blue_ribbon))

# Test tagged() function
t2 = t1.tagged(red_ribbon)
print(t2.tags)
assert t2.tags == frozenset((reserve_best_in_show_ribbon, best_in_class_ribbon,
blue_ribbon, red_ribbon))

# Test that tagged() fails if a UniqueTag of the same subclass
# is alredy present
with pytest.raises(NonUniqueTagError):
t1.tagged(best_in_show_ribbon)

# Test without_tags() function
t4 = t2.without_tags(red_ribbon)
assert t4.tags == t1.tags

# Test that without_tags() fails if the tag is not present.
with pytest.raises(ValueError):
t4.without_tags(red_ribbon)


if __name__ == "__main__":
if len(sys.argv) > 1:
exec(sys.argv[1])
Expand Down