Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Property setters are now distinguished from getters via their callname #75

Merged
merged 8 commits into from
Jul 10, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 3 additions & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ jobs:
name: run tests
command: |
. venv/bin/activate
pip install pytest-cov==2.8.1 # hack to avoid regression
python run_tests.py
- store_artifacts:
path: test-reports
Expand Down Expand Up @@ -105,6 +106,7 @@ jobs:
name: run tests
command: |
. venv/bin/activate
pip install pytest-cov==2.8.1 # hack to avoid regression
python run_tests.py
- store_artifacts:
path: test-reports
Expand Down Expand Up @@ -243,6 +245,7 @@ jobs:
pip install -r requirements/runtime.txt
pip install -e .

pip install pytest-cov==2.8.1 # hack to avoid regression
./run_doctests.sh || echo "pypy failed, but this is allowed"
./run_tests.sh || echo "pypy failed, but this is allowed"
"
11 changes: 11 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,18 @@ before_install:
install:
- travis_retry pip install -e .


# Broken on pytest-cov-2.10.0
# Broken on pytest==5.4.3
# Broken on pytest==5.3.5
# Broken on pytest==5.2.4
#pip install pytest-cov==2.8.1
#pip install pytest-cov==2.8.1
#pip install pytest-cov==2.10.0

script:
- pip uninstall pytest-cov
- pip install pytest-cov==2.8.1
- travis_wait pytest --cov=xdoctest

after_success:
Expand Down
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,11 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm

### Changed
* `xdoctest.runner.doctest_module` now accepts the module object itself.
* zero-args doctests no longer capture stdout (this prevents IPython embedding issues).
* Zero-args doctests no longer capture stdout (this prevents IPython embedding issues).

### Fixed
* Fixed minor bug in zero args runner when captured stdout is None
* We now ignore doctests in setters and deleters to prevent them from clobbering doctests in getters.


## [Version 0.12.0] - Released 2020-04-16
Expand Down
1 change: 1 addition & 0 deletions appveyor.yml
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ test_script:
# the interpreter you're using - Appveyor does not do anything special
# to put the Python version you want to use on PATH.
- set PYTHONIOENCODING=utf-8
- "%PYTHON%\\python.exe -m pip install pytest-cov==2.8.1"
- "%PYTHON%\\python.exe -m xdoctest xdoctest"
- "%PYTHON%\\python.exe -m pytest"

Expand Down
115 changes: 115 additions & 0 deletions dev/demo_properties.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
# This demonstrates dynamics of properties with clobbering any
# namespace variables, so its entirely clear what variables exist and how they
# are transformed as the property dectorators are applied
import pytest


def my_getter_func(self):
" getter doc"
print('call getter')
return 'value'


def my_setter_func(self, value):
" setter doc"
print('call setter for value = {!r}'.format(value))


def my_deleter_func(self):
" deleter doc"
print('call deleter')


# Use the property decorator directly with normal call syntax
# Note properties --- like most other decorators --- return new function
# objects and do not change the underlying function object. Hense we can still
# print the original functions and see how they are assigned to the fset / fget
# / fdel attributes of the returned property object.
my_getter_prop = property(my_getter_func)
my_setter_prop = my_getter_prop.setter(my_setter_func)
my_deleter_prop = my_setter_prop.deleter(my_deleter_func)

print('my_getter_func = {!r}'.format(my_getter_func))
print('my_setter_func = {!r}'.format(my_setter_func))
print('my_deleter_func = {!r}'.format(my_deleter_func))

print('my_getter_prop = {!r}'.format(my_getter_prop))
print('my_setter_prop = {!r}'.format(my_setter_prop))
print('my_deleter_prop = {!r}'.format(my_deleter_prop))

print('my_getter_prop = {!r}'.format(my_getter_prop))
print('my_getter_prop.fget = {!r}'.format(my_getter_prop.fget))
print('my_getter_prop.fset = {!r}'.format(my_getter_prop.fset))
print('my_getter_prop.fdel = {!r}'.format(my_getter_prop.fdel))
print('my_getter_prop.__doc__ = {!r}'.format(my_getter_prop.__doc__))

print('my_setter_prop = {!r}'.format(my_setter_prop))
print('my_setter_prop.fget = {!r}'.format(my_setter_prop.fget))
print('my_setter_prop.fset = {!r}'.format(my_setter_prop.fset))
print('my_setter_prop.fdel = {!r}'.format(my_setter_prop.fdel))
print('my_setter_prop.__doc__ = {!r}'.format(my_setter_prop.__doc__))

print('my_deleter_prop = {!r}'.format(my_deleter_prop))
print('my_deleter_prop.fget = {!r}'.format(my_deleter_prop.fget))
print('my_deleter_prop.fset = {!r}'.format(my_deleter_prop.fset))
print('my_deleter_prop.fdel = {!r}'.format(my_deleter_prop.fdel))
print('my_deleter_prop.__doc__ = {!r}'.format(my_deleter_prop.__doc__))

# Note: each function has its own doc, but only the doc of the getter is stored

# my_getter_func = <function my_getter_func at 0x7f2f14a6d700>
# my_setter_func = <function my_setter_func at 0x7f2f14a6d0d0>
# my_deleter_func = <function my_deleter_func at 0x7f2f14b88ee0>
# my_getter_prop = <property object at 0x7f2f14c7b180>
# my_setter_prop = <property object at 0x7f2f14d318b0>
# my_deleter_prop = <property object at 0x7f2f14c81e00>
# my_getter_prop = <property object at 0x7f2f14c7b180>
# my_getter_prop.fget = <function my_getter_func at 0x7f2f14a6d700>
# my_getter_prop.fset = None
# my_getter_prop.fdel = None
# my_getter_prop.__doc__ = ' getter doc'
# my_setter_prop = <property object at 0x7f2f14d318b0>
# my_setter_prop.fget = <function my_getter_func at 0x7f2f14a6d700>
# my_setter_prop.fset = <function my_setter_func at 0x7f2f14a6d0d0>
# my_setter_prop.fdel = None
# my_setter_prop.__doc__ = ' getter doc'
# my_deleter_prop = <property object at 0x7f2f14c81e00>
# my_deleter_prop.fget = <function my_getter_func at 0x7f2f14a6d700>
# my_deleter_prop.fset = <function my_setter_func at 0x7f2f14a6d0d0>
# my_deleter_prop.fdel = <function my_deleter_func at 0x7f2f14b88ee0>
# my_deleter_prop.__doc__ = ' getter doc'


# Create an empty type
class Husk:
pass

# Assigning properties to the class itself is equivalent to how they are
# normally defined in the scope of the class definition.
Husk.x = my_deleter_prop
Husk.y = my_setter_prop
Husk.z = my_getter_prop


# Creating an instance of the class will let us use our property variables
self = Husk()

# The "deleter" property has fget, fset, and fdel defined
self.x
self.x = 3
del self.x


# The "setter" property only had fget and fset defined
self.y
self.y = 3
with pytest.raises(AttributeError):
del self.y


# The "getter" property only had fget defined
self = Husk()
self.z
with pytest.raises(AttributeError):
self.z = 3
del self.z
113 changes: 113 additions & 0 deletions testing/test_cases.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
from xdoctest.utils.util_misc import _run_case
from xdoctest import utils


def test_properties():
"""
Test that all doctests are extracted from properties correctly.
https://github.com/Erotemic/xdoctest/issues/73

Credit: @trappitsch
"""
text = _run_case(utils.codeblock(
'''
class Test(object):
@property
def test(self):
"""
Example:
>>> ini = Test()
>>> ini.test
3.14
"""
return 3.14

@test.setter
def test(self, s):
pass
'''))
assert 'running 1 test' in text

text = _run_case(utils.codeblock(
'''
class Test(object):
@property
def test(self):
"""
Example:
>>> ini = Test()
>>> ini.test
3.14
"""
return 3.14
'''))
assert 'running 1 test' in text

text = _run_case(utils.codeblock(
'''
class Test(object):
@property
def test(self):
"""
Example:
>>> ini = Test()
>>> ini.test
3.14
"""
return 3.14

@test.setter
def test(self, s):
"""
Example:
>>> ini = Test()
>>> ini.test = 3
"""
pass
'''))
assert 'running 1 test' in text

text = _run_case(utils.codeblock(
'''
class Test(object):
@property
def test(self):
return 3.14

@test.setter
def test(self, s):
"""
Example:
>>> ini = Test()
>>> ini.test = 3
"""
pass
'''))
assert 'running 0 test' in text

text = _run_case(utils.codeblock(
'''
class Test(object):
@property
def test(self):
return 3.14

@test.setter
def test(self, s):
"""
Example:
>>> ini = Test()
>>> ini.test = 3
"""
pass

@test.deleter
def test(self, s):
"""
Example:
>>> ini = Test()
>>> ini.test = 3
"""
pass
'''))
assert 'running 0 test' in text
42 changes: 42 additions & 0 deletions testing/test_limitations.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
"""
Tests in this file are more demonstrations of the limitations of the static
analysis doctest parser.
"""
from xdoctest import utils
from xdoctest.utils.util_misc import _run_case


def test_pathological_property_case():
"""
This case actually wont error, but it displays a limitation of static
analysis. We trick the doctest node parser into thinking there is a setter
property when there really isn't.
"""
text = _run_case(utils.codeblock(
'''
def property(x):
class Foo():
def setter(self, x):
return x
return Foo()

class Test(object):
@property
def test(self):
"""
Example:
>>> print('not really a getter')
"""
return 3.14

@test.setter
def test(self, s):
"""
Example:
>>> print('not really a setter')
"""
pass
'''))
# If there was a way to make this case fail, that would be ok
assert 'Test.test:0' in text
# assert 'Test.test.fset:0' in text
2 changes: 0 additions & 2 deletions testing/test_traceback.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
# -*- coding: utf-8 -*-
"""
Need to enhance the tracebacks to spit out something more useful

TODO: rename to test traceback
"""
from xdoctest import utils
from xdoctest.utils.util_misc import _run_case
Expand Down