josiahcarlson / mprop

Module properties for Python
GNU Lesser General Public License v2.1
24 stars 0 forks source link

Instance of `mproperty` cannot contain both `setter` and `deleter` #3

Closed TezRomacH closed 5 years ago

TezRomacH commented 5 years ago

I would like to create a one global module property with the getter but without setter and deleter.

I run this simple code

class _relative_:
    def __str__(self) -> str:
        return "*"
_relative_instance = _relative()

@mproperty
def relative(module):
    return _relative_instance

@relative.setter
def _setter(module, value) -> NoReturn:
    raise ValueError("Cannot set a value into relative")

@relative.deleter
def _deleter(module, deleter) -> NoReturn:
    raise ValueError("Cannot delete relative")

But when i try to import my module i got an error

...
    module = self._system_import(name, *args, **kwargs)
  File "/Users/romach/Documents/aiml/safitty/safitty/core.py", line 27, in <module>
    @relative.deleter
NameError: name 'relative' is not defined

I tried to change the order of functions and create _deleter before _setter:

....
  File "/Users/romach/Documents/aiml/safitty/safitty/core.py", line 27, in <module>
    @relative.setter
NameError: name 'relative' is not defined
TezRomacH commented 5 years ago

moreover, if I remove one of them, i will be able to import the module, but neither setter nor deleter won't work and will not raise ValueError

josiahcarlson commented 5 years ago

Thank you for the bug report, this is an example of doing things too early. Long story short, I transformed the module and the property itself too early, and it's trying to fetch the property in order to set the setter.

On your side, your setter/deleter calls are wrong in the context of Python properties (they won't work the way you seem to expect, even with regular properties on a class). This below works with the updated and released 0.16.0, available now at PyPI and here on the master branch.

from typing import NoReturn
from mprop import mproperty

class _relative:
    def __str__(self) -> str:
        return "*"
_relative_instance = _relative()

@mproperty
def relative(module):
    return _relative_instance

@relative.setter
def relative(module, value) -> NoReturn:
    raise ValueError("Cannot set a value into relative")

@relative.deleter
def relative(module, deleter) -> NoReturn:
    raise ValueError("Cannot delete relative")

But really, if all you wanted to do was to prevent the assignment, that already works with the way properties normally work. No setter, can't set. No deleter, can't delete.

#example3.py
import mprop
@mprop.mproperty
def foo(self):
    return "hello"

In a console:

$ python3.6
Python 3.6.3 (default, Apr  4 2018, 14:57:55) 
[GCC 5.4.0 20160609] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import example3
>>> example3.foo
'hello'
>>> example3.foo = 'bar'
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: can't set attribute
>>> del example3.foo
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: can't delete attribute

Thank you again for the bug report.

TezRomacH commented 5 years ago

Thank you for a quick response. I found interesting details about mproperty. Pip version == 0.16.0

My module was organized like:

safitty/
    __init__.py
    core.py

In core module I created property without any setter and deleter as you note before.

@mprop.mproperty
def relative(module):
    return _relative_instance

and then in __init__.py:

from safitty.core import relative

__all__ = ['relative']

with this, after import my module I still able to delete and set any value. But if I move the definition of relative into __init__.py:

@mprop.mproperty
def relative(module):
    return _relative_instance

__all__ = ['relative']

Everything works well. Is this the expected behavior?

josiahcarlson commented 5 years ago

The behavior you report is expected by me. If you want to expect it too, you can read the descriptor docs listed in 1 below, or believe me in that it implies 2 and 3, and explains why you get what you get. I have added the additional semantics in 4 with this library, which further explains.

  1. Read: https://docs.python.org/3/howto/descriptor.html
  2. Running from x import y inside of file z.py means you look up the attribute / descriptor y on x once, then you cache the reference inside z
  3. y is no longer a descriptor when accessed from the importing module z, but is still a descriptor when accessed via x.y from z or anywhere else that imported x.
  4. To access y from inside x as a property, use _pmodule.y, because you still need an object context to get a descriptor properly.