diff --git a/docs/examples.md b/docs/examples.md index 1154d7a13..465dc3952 100644 --- a/docs/examples.md +++ b/docs/examples.md @@ -408,18 +408,16 @@ Therefore if you use `@validator`, it is *not* enough to annotate said attribute *attrs* ships with a bunch of validators, make sure to [check them out](api-validators) before writing your own: -```{eval-rst} -.. doctest:: - - >>> @define - ... class C: - ... x: int = field(validator=validators.instance_of(int)) - >>> C(42) - C(x=42) - >>> C("42") - Traceback (most recent call last): - ... - TypeError: ("'x' must be (got '42' that is a ).", Attribute(name='x', default=NOTHING, factory=NOTHING, validator=>, type=None, kw_only=False), , '42') +```{doctest} +>>> @define +... class C: +... x: int = field(validator=validators.instance_of(int)) +>>> C(42) +C(x=42) +>>> C("42") +Traceback (most recent call last): + ... +TypeError: ("'x' must be (got '42' that is a ).", Attribute(name='x', default=NOTHING, factory=NOTHING, validator=>, type=None, kw_only=False), , '42') ``` Please note that if you use {func}`attr.s` (and **not** {func}`attrs.define`) to define your class, validators only run on initialization by default -- not when you set an attribute. diff --git a/docs/extending.md b/docs/extending.md index 6ec60848e..84bd9bfe3 100644 --- a/docs/extending.md +++ b/docs/extending.md @@ -160,7 +160,7 @@ Here are some tips for effective use of metadata: - To avoid metadata key collisions, consider exposing your metadata keys from your modules.: - ``` + ```python from mylib import MY_METADATA_KEY @define @@ -173,31 +173,28 @@ Here are some tips for effective use of metadata: - Expose `field` wrappers for your specific metadata. This is a more graceful approach if your users don't require metadata from other libraries. - ```{eval-rst} - .. doctest:: - - >>> from attr import fields, NOTHING - >>> MY_TYPE_METADATA = '__my_type_metadata' - >>> - >>> def typed( - ... cls, default=NOTHING, validator=None, repr=True, - ... eq=True, order=None, hash=None, init=True, metadata=None, - ... converter=None - ... ): - ... metadata = metadata or {} - ... metadata[MY_TYPE_METADATA] = cls - ... return field( - ... default=default, validator=validator, repr=repr, - ... eq=eq, order=order, hash=hash, init=init, - ... metadata=metadata, converter=converter - ... ) - >>> - >>> @define - ... class C: - ... x: int = typed(int, default=1, init=False) - >>> fields(C).x.metadata[MY_TYPE_METADATA] - - + ```{doctest} + >>> from attrs import fields, NOTHING + >>> MY_TYPE_METADATA = '__my_type_metadata' + >>> + >>> def typed( + ... cls, default=NOTHING, validator=None, repr=True, + ... eq=True, order=None, hash=None, init=True, metadata=None, + ... converter=None + ... ): + ... metadata = metadata or {} + ... metadata[MY_TYPE_METADATA] = cls + ... return field( + ... default=default, validator=validator, repr=repr, + ... eq=eq, order=order, hash=hash, init=init, + ... metadata=metadata, converter=converter + ... ) + >>> + >>> @define + ... class C: + ... x: int = typed(int, default=1, init=False) + >>> fields(C).x.metadata[MY_TYPE_METADATA] + ``` (transform-fields)= @@ -293,24 +290,22 @@ Data(public=42, _private='spam', explicit='yes') *attrs* allows you to serialize instances of *attrs* classes to dicts using the {func}`attrs.asdict` function. However, the result can not always be serialized since most data types will remain as they are: -```{eval-rst} -.. doctest:: - - >>> import json - >>> import datetime - >>> from attrs import asdict - >>> - >>> @frozen - ... class Data: - ... dt: datetime.datetime +```{doctest} +>>> import json +>>> import datetime +>>> from attrs import asdict +>>> +>>> @frozen +... class Data: +... dt: datetime.datetime +... +>>> data = asdict(Data(datetime.datetime(2020, 5, 4, 13, 37))) +>>> data +{'dt': datetime.datetime(2020, 5, 4, 13, 37)} +>>> json.dumps(data) +Traceback (most recent call last): ... - >>> data = asdict(Data(datetime.datetime(2020, 5, 4, 13, 37))) - >>> data - {'dt': datetime.datetime(2020, 5, 4, 13, 37)} - >>> json.dumps(data) - Traceback (most recent call last): - ... - TypeError: Object of type datetime is not JSON serializable +TypeError: Object of type datetime is not JSON serializable ``` To help you with this, {func}`~attrs.asdict` allows you to pass a *value_serializer* hook. diff --git a/docs/glossary.md b/docs/glossary.md index 468cfb7b6..0ae266167 100644 --- a/docs/glossary.md +++ b/docs/glossary.md @@ -26,20 +26,18 @@ slotted classes - Slotted classes don't allow for any other attribute to be set except for those defined in one of the class' hierarchies `__slots__`: - ```{eval-rst} - .. doctest:: - - >>> from attr import define - >>> @define - ... class Coordinates: - ... x: int - ... y: int - ... - >>> c = Coordinates(x=1, y=2) - >>> c.z = 3 - Traceback (most recent call last): - ... - AttributeError: 'Coordinates' object has no attribute 'z' + ```{doctest} + >>> from attr import define + >>> @define + ... class Coordinates: + ... x: int + ... y: int + ... + >>> c = Coordinates(x=1, y=2) + >>> c.z = 3 + Traceback (most recent call last): + ... + AttributeError: 'Coordinates' object has no attribute 'z' ``` - Slotted classes can inherit from other classes just like non-slotted classes, but some of the benefits of slotted classes are lost if you do that. diff --git a/docs/how-does-it-work.md b/docs/how-does-it-work.md index 4715887cb..72972e753 100644 --- a/docs/how-does-it-work.md +++ b/docs/how-does-it-work.md @@ -20,18 +20,16 @@ While creating new classes is more elegant, we've run into several edge cases su To be very clear: if you define a class with a single attribute without a default value, the generated `__init__` will look *exactly* how you'd expect: -```{eval-rst} -.. doctest:: - - >>> import inspect - >>> from attr import define - >>> @define - ... class C: - ... x: int - >>> print(inspect.getsource(C.__init__)) - def __init__(self, x): - self.x = x - +```{doctest} +>>> import inspect +>>> from attrs import define +>>> @define +... class C: +... x: int +>>> print(inspect.getsource(C.__init__)) +def __init__(self, x): + self.x = x + ``` No magic, no meta programming, no expensive introspection at runtime. diff --git a/docs/types.md b/docs/types.md index 9bbb7fd97..b1c14dbad 100644 --- a/docs/types.md +++ b/docs/types.md @@ -4,19 +4,17 @@ However they will forever remain *optional*, therefore the example from the README could also be written as: -```{eval-rst} -.. doctest:: +```{doctest} +>>> from attrs import define, field - >>> from attrs import define, field +>>> @define +... class SomeClass: +... a_number = field(default=42) +... list_of_numbers = field(factory=list) - >>> @define - ... class SomeClass: - ... a_number = field(default=42) - ... list_of_numbers = field(factory=list) - - >>> sc = SomeClass(1, [1, 2, 3]) - >>> sc - SomeClass(a_number=1, list_of_numbers=[1, 2, 3]) +>>> sc = SomeClass(1, [1, 2, 3]) +>>> sc +SomeClass(a_number=1, list_of_numbers=[1, 2, 3]) ``` You can choose freely between the approaches, but please remember that if you choose to use type annotations, you **must** annotate **all** attributes! diff --git a/docs/why.md b/docs/why.md index c557fa02a..ed896f113 100644 --- a/docs/why.md +++ b/docs/why.md @@ -59,32 +59,28 @@ However, that convenience comes at a price. The most obvious difference between `namedtuple`s and *attrs*-based classes is that the latter are type-sensitive: -```{eval-rst} -.. doctest:: - - >>> import attr - >>> C1 = attr.make_class("C1", ["a"]) - >>> C2 = attr.make_class("C2", ["a"]) - >>> i1 = C1(1) - >>> i2 = C2(1) - >>> i1.a == i2.a - True - >>> i1 == i2 - False +```{doctest} +>>> import attrs +>>> C1 = attrs.make_class("C1", ["a"]) +>>> C2 = attrs.make_class("C2", ["a"]) +>>> i1 = C1(1) +>>> i2 = C2(1) +>>> i1.a == i2.a +True +>>> i1 == i2 +False ``` -…while a `namedtuple` is *intentionally* [behaving like a tuple] which means the type of a tuple is *ignored*: +…while a `namedtuple` is *intentionally* [behaving like a tuple](https://docs.python.org/3/tutorial/datastructures.html#tuples-and-sequences) which means the type of a tuple is *ignored*: -```{eval-rst} -.. doctest:: - - >>> from collections import namedtuple - >>> NT1 = namedtuple("NT1", "a") - >>> NT2 = namedtuple("NT2", "b") - >>> t1 = NT1(1) - >>> t2 = NT2(1) - >>> t1 == t2 == (1,) - True +```{doctest} +>>> from collections import namedtuple +>>> NT1 = namedtuple("NT1", "a") +>>> NT2 = namedtuple("NT2", "b") +>>> t1 = NT1(1) +>>> t2 = NT2(1) +>>> t1 == t2 == (1,) +True ``` Other often surprising behaviors include: @@ -177,7 +173,7 @@ Why would you want to write `customer[2]` instead of `customer.first_name`? Don't get me started when you add nesting. If you've never run into mysterious tuples you had no idea what the hell they meant while debugging, you're much smarter than yours truly. -Using proper classes with names and types makes program code much more readable and [comprehensible]. +Using proper classes with names and types makes program code much more readable and [comprehensible](https://arxiv.org/pdf/1304.5257.pdf). Especially when trying to grok a new piece of software or returning to old code after several months. @@ -210,71 +206,67 @@ I usually manage to get some typos inside and there's simply more code that can To bring it into perspective, the equivalent of -```{eval-rst} -.. doctest:: - - >>> @attr.s - ... class SmartClass: - ... a = attr.ib() - ... b = attr.ib() - >>> SmartClass(1, 2) - SmartClass(a=1, b=2) +```{doctest} +>>> @attrs.define +... class SmartClass: +... a = attrs.field() +... b = attrs.field() +>>> SmartClass(1, 2) +SmartClass(a=1, b=2) ``` is roughly -```{eval-rst} -.. doctest:: - - >>> class ArtisanalClass: - ... def __init__(self, a, b): - ... self.a = a - ... self.b = b - ... - ... def __repr__(self): - ... return f"ArtisanalClass(a={self.a}, b={self.b})" - ... - ... def __eq__(self, other): - ... if other.__class__ is self.__class__: - ... return (self.a, self.b) == (other.a, other.b) - ... else: - ... return NotImplemented - ... - ... def __ne__(self, other): - ... result = self.__eq__(other) - ... if result is NotImplemented: - ... return NotImplemented - ... else: - ... return not result - ... - ... def __lt__(self, other): - ... if other.__class__ is self.__class__: - ... return (self.a, self.b) < (other.a, other.b) - ... else: - ... return NotImplemented - ... - ... def __le__(self, other): - ... if other.__class__ is self.__class__: - ... return (self.a, self.b) <= (other.a, other.b) - ... else: - ... return NotImplemented - ... - ... def __gt__(self, other): - ... if other.__class__ is self.__class__: - ... return (self.a, self.b) > (other.a, other.b) - ... else: - ... return NotImplemented - ... - ... def __ge__(self, other): - ... if other.__class__ is self.__class__: - ... return (self.a, self.b) >= (other.a, other.b) - ... else: - ... return NotImplemented - ... - ... def __hash__(self): - ... return hash((self.__class__, self.a, self.b)) - >>> ArtisanalClass(a=1, b=2) - ArtisanalClass(a=1, b=2) +```{doctest} +>>> class ArtisanalClass: +... def __init__(self, a, b): +... self.a = a +... self.b = b +... +... def __repr__(self): +... return f"ArtisanalClass(a={self.a}, b={self.b})" +... +... def __eq__(self, other): +... if other.__class__ is self.__class__: +... return (self.a, self.b) == (other.a, other.b) +... else: +... return NotImplemented +... +... def __ne__(self, other): +... result = self.__eq__(other) +... if result is NotImplemented: +... return NotImplemented +... else: +... return not result +... +... def __lt__(self, other): +... if other.__class__ is self.__class__: +... return (self.a, self.b) < (other.a, other.b) +... else: +... return NotImplemented +... +... def __le__(self, other): +... if other.__class__ is self.__class__: +... return (self.a, self.b) <= (other.a, other.b) +... else: +... return NotImplemented +... +... def __gt__(self, other): +... if other.__class__ is self.__class__: +... return (self.a, self.b) > (other.a, other.b) +... else: +... return NotImplemented +... +... def __ge__(self, other): +... if other.__class__ is self.__class__: +... return (self.a, self.b) >= (other.a, other.b) +... else: +... return NotImplemented +... +... def __hash__(self): +... return hash((self.__class__, self.a, self.b)) +>>> ArtisanalClass(a=1, b=2) +ArtisanalClass(a=1, b=2) ``` which is quite a mouthful and it doesn't even use any of *attrs*'s more advanced features like validators or defaults values. @@ -284,18 +276,16 @@ And who will guarantee you, that you don't accidentally flip the `<` in your ten It also should be noted that *attrs* is not an all-or-nothing solution. You can freely choose which features you want and disable those that you want more control over: -```{eval-rst} -.. doctest:: - - >>> @attr.s(repr=False) - ... class SmartClass: - ... a = attr.ib() - ... b = attr.ib() - ... - ... def __repr__(self): - ... return "" % (self.a,) - >>> SmartClass(1, 2) - +```{doctest} +>>> @attrs.define +... class SmartClass: +... a: int +... b: int +... +... def __repr__(self): +... return "" % (self.a,) +>>> SmartClass(1, 2) + ``` :::{admonition} Summary @@ -305,6 +295,3 @@ However it takes a lot of bias and determined rationalization to claim that *att In any case, if you ever get sick of the repetitiveness and drowning important code in a sea of boilerplate, *attrs* will be waiting for you. ::: - -[behaving like a tuple]: https://docs.python.org/3/tutorial/datastructures.html#tuples-and-sequences -[comprehensible]: https://arxiv.org/pdf/1304.5257.pdf