On Thu, Sep 17, 2020 at 08:34:26PM -0000, Joseph Perez wrote:

> A lot of libraries use string for attribute names to do some "dynamic" 
> things. A typical example are SQLAlchemy 
> [validators](https://docs.sqlalchemy.org/en/13/orm/mapped_attributes.html#simple-validators):
> ```python
> from sqlalchemy.orm import validates
> 
> class EmailAddress(Base):
>     __tablename__ = 'address'

Dunder names are reserved for use by the Python interpreter. Your code 
here is already on very shaky ground.


>     id = Column(Integer, primary_key=True)
>     email = Column(String)
> 
>     @validates('email')  # Here
>     def validate_email(self, key, address):
>         assert '@' in address
>         return address
> 
> ```
> In this example of SQLAlchemy documentation, email validator use 
> `"email"` string in order to associate the validator to the `email` 
> column.
>
> However this dynamic things don't play well with all static tools like 
> linters, but especially IDE (for usages finding, navigation, 
> refactoring, etc.), and of course type checkers.

This is the trouble with writing "clever" code with large amounts of 
implicit state and/or dynamicism, it makes it difficult for static tools 
and the human reader.


> This issue could be solved with a "magic attribute" `__attrs__` (name 
> can be discussed), used the following way:

I don't see how this would solve the issue. The problem is that your 
code is too dynamic for static tools, so you are proposing to make it 
**even more dynamic**.


> ```python
> @dataclass
> class Foo:
>     bar: int
> 
> foo = Foo(0)
> assert getattr(foo, Foo.__attrs__.bar) == 0
> assert getattr(foo, foo.__attrs__.bar) == 0
> ```


Presumably these would work too:

    assert getattr(foo, None.__attrs__.bar) == 0
    assert getattr(foo, foo.__attrs__.baz, 999) == 999

Your proposal is to have a magic dunder, spelled '__attrs__` (note the 
two additional dots) which converts a attribute access into a string:

    spam.__attrs__.eggs => 'eggs'

so you can pass the 'eggs' string to getattr:

    # Instead of this:
    getattr(spam, 'eggs')

    # this is more magical:
    getattr(spam, spam.__attrs__.eggs)

Apart from typing 15 chars to avoid 2 quotation marks, and making a 
runtime attribute lookup plus method call to avoid a literal, how will 
this help reduce dynamicism?

If linters etc have difficulty dealing with getattr() using a string 
literal, how will they do better at even more complex, even more 
dynamic, code?


> To make it usable in class declaration, the `__attrs__` symbol should 
> be added to class declaration namespace:
>
> ```python
> class Foo:
>     bar: int
>     @validator(__attrs__.bar)
>     def validate(self):
>         ...
> ```

So here we have `__attrs__` is not just a dunder attribute, but also a 
dunder built-in name.


> No check would be done by `__attrs__`, they are let to linters which 
> would integrate this language feature and check for the presence of 
> the attribute in the class/instance.

So linters that cannot cope with `@validate('email')` will be able to 
cope with `@validate(__attrs__.email)`, because ... why?

I don't think that static tools have trouble with dynamic code because 
they cannot parse a string literal like `'email'`. Removing that literal 
with more dynamic code like `__attrs__.email` is not going to solve the 
problem.


[...]
> To sum up, `__attrs__` would be a pseudo-static wrapper to attribute 
> name retrieving in order to benefit of static tools (refactoring, 
> usages finding, navigation, type checking), with a straightforward and 
> backward compatible implementation.

You have made a lot of claims that this will help static tools. I do not 
believe it will help. I do not expect that static tools that cannot 
analyse dynamic code when given a string literal in a dynamic context 
will magically start working when you replace that string literal with 
an extra level of dynamic code.

To me, this just adds one more step to any static tool: any time the 
tool, or the human reader, sees

    __attrs__.spam

mentally erase the "__attrs__." and quote the remaining:

    'spam'

and then proceed as normal from that point. Likewise for attributes:

    foo.__attrs__.spam

If `foo` exists, erase "foo.__attrs__." and quote the remaining part, 
then proceed as normal. Otherwise raise NameError.

Essentially this `__attrs__` is a verbose way of spelling string 
literals, and can be removed with a pretty simple text substitution.


-- 
Steve
_______________________________________________
Python-ideas mailing list -- python-ideas@python.org
To unsubscribe send an email to python-ideas-le...@python.org
https://mail.python.org/mailman3/lists/python-ideas.python.org/
Message archived at 
https://mail.python.org/archives/list/python-ideas@python.org/message/O5MGEXUMVXDDZZUTVCLKC5U7SN7SQYGY/
Code of Conduct: http://python.org/psf/codeofconduct/

Reply via email to