diff --git a/CHANGELOG.md b/CHANGELOG.md index 9dedf443..67b37e21 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,16 @@ We are currently working on porting this changelog to the specifications in This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## Version 1.1.4 - Unreleased +## Version 1.1.5 - Unreleased + +### Changed +* Minor modification to `xdoctest --version-info` and exposed it in CLI help. + +### Fixed +* `ub.modname_to_modpath` fixed in cases where editable installs use type annotations in their MAPPING definition. + + +## Version 1.1.4 - Released 2024-05-31 ### Fixed * Working around a `modname_to_modpath` issue. diff --git a/dev/demo/demo_dynamic_analysis.py b/dev/demo/demo_dynamic_analysis.py new file mode 100644 index 00000000..af12026b --- /dev/null +++ b/dev/demo/demo_dynamic_analysis.py @@ -0,0 +1,18 @@ +""" +CommandLine: + xdoctest ~/code/xdoctest/dev/demo/demo_dynamic_analysis.py --analysis=auto + + xdoctest ~/code/xdoctest/dev/demo/demo_dynamic_analysis.py --analysis=dynamic + + xdoctest ~/code/xdoctest/dev/demo/demo_dynamic_analysis.py --xdoc-force-dynamic +""" + + +def func() -> None: + r''' Dynamic doctest + >>> %s + %s + ''' + return + +func.__doc__ %= ('print(1)', '1') diff --git a/dev/demo_errors.py b/dev/demo/demo_errors.py similarity index 79% rename from dev/demo_errors.py rename to dev/demo/demo_errors.py index 12fa530a..e89fb6b8 100644 --- a/dev/demo_errors.py +++ b/dev/demo/demo_errors.py @@ -9,7 +9,7 @@ def demo1(): """ CommandLine: - xdoctest -m ~/code/xdoctest/dev/demo_errors.py demo1 + xdoctest -m ~/code/xdoctest/dev/demo/demo_errors.py demo1 Example: >>> raise Exception('demo1') @@ -20,7 +20,7 @@ def demo1(): def demo2(): """ CommandLine: - xdoctest -m ~/code/xdoctest/dev/demo_errors.py demo2 + xdoctest -m ~/code/xdoctest/dev/demo/demo_errors.py demo2 Example: >>> print('error on different line') @@ -32,7 +32,7 @@ def demo2(): def demo3(): """ CommandLine: - xdoctest -m ~/code/xdoctest/dev/demo_errors.py demo3 + xdoctest -m ~/code/xdoctest/dev/demo/demo_errors.py demo3 Example: >>> print('demo5') @@ -44,7 +44,7 @@ def demo3(): class Demo5(object): """ CommandLine: - xdoctest -m ~/code/xdoctest/dev/demo_errors.py Demo5 + xdoctest -m ~/code/xdoctest/dev/demo/demo_errors.py Demo5 Example: >>> raise Exception @@ -52,7 +52,7 @@ class Demo5(object): def demo5(self): """ CommandLine: - xdoctest -m ~/code/xdoctest/dev/demo_errors.py Demo5.demo5 + xdoctest -m ~/code/xdoctest/dev/demo/demo_errors.py Demo5.demo5 Example: >>> raise Exception @@ -100,7 +100,7 @@ def demo_runtime_warning(): if __name__ == '__main__': """ CommandLine: - python ~/code/xdoctest/dev/demo_errors.py all + python ~/code/xdoctest/dev/demo/demo_errors.py all """ import xdoctest xdoctest.doctest_module(__file__) diff --git a/dev/demo_issues.py b/dev/demo/demo_issues.py similarity index 79% rename from dev/demo_issues.py rename to dev/demo/demo_issues.py index b6464cfd..748c29ae 100644 --- a/dev/demo_issues.py +++ b/dev/demo/demo_issues.py @@ -21,16 +21,16 @@ def demo(): # Correctly reports skipped (although an only skipped test report # should probably be yellow) - xdoctest -m dev/demo_issues.py demo_requires_skips_all_v1 + xdoctest -m dev/demo/demo_issues.py demo_requires_skips_all_v1 # Incorrectly reports success - xdoctest -m dev/demo_issues.py demo_requires_skips_all_v2 + xdoctest -m dev/demo/demo_issues.py demo_requires_skips_all_v2 # Correctly reports success - xdoctest -m dev/demo_issues.py demo_requires_skips_all_v2 --cliflag + xdoctest -m dev/demo/demo_issues.py demo_requires_skips_all_v2 --cliflag # Correctly reports success - xdoctest -m dev/demo_issues.py demo_requires_skips_all_v1 --cliflag + xdoctest -m dev/demo/demo_issues.py demo_requires_skips_all_v1 --cliflag """ # Programatic reproduction (notice the first one also reports itself in @@ -40,7 +40,7 @@ def demo(): xdoctest.doctest_callable(demo_requires_skips_all_v2) import sys, ubelt - sys.path.append(ubelt.expandpath('~/code/xdoctest/dev')) + sys.path.append(ubelt.expandpath('~/code/xdoctest/dev/demo')) import demo_issues # Correctly reports skipped diff --git a/dev/demo_properties.py b/dev/demo/demo_properties.py similarity index 100% rename from dev/demo_properties.py rename to dev/demo/demo_properties.py diff --git a/dev/demo_usage_with_logger.py b/dev/demo/demo_usage_with_logger.py similarity index 89% rename from dev/demo_usage_with_logger.py rename to dev/demo/demo_usage_with_logger.py index ca8630a3..3bac805c 100644 --- a/dev/demo_usage_with_logger.py +++ b/dev/demo/demo_usage_with_logger.py @@ -6,13 +6,13 @@ CommandLine: # Run with xdoctest runner - xdoctest ~/code/xdoctest/dev/demo_usage_with_logger.py + xdoctest ~/code/xdoctest/dev/demo/demo_usage_with_logger.py # Run with pytest runner - pytest -s --xdoctest --xdoctest-verbose=3 ~/code/xdoctest/dev/demo_usage_with_logger.py + pytest -s --xdoctest --xdoctest-verbose=3 ~/code/xdoctest/dev/demo/demo_usage_with_logger.py # Run with builtin main - python ~/code/xdoctest/dev/demo_usage_with_logger.py + python ~/code/xdoctest/dev/demo/demo_usage_with_logger.py References: .. [Issue111] https://github.com/Erotemic/xdoctest/issues/111 diff --git a/dev/demo_dynamic_analysis.py b/dev/demo_dynamic_analysis.py deleted file mode 100644 index bfce0845..00000000 --- a/dev/demo_dynamic_analysis.py +++ /dev/null @@ -1,18 +0,0 @@ -""" -CommandLine: - xdoctest ~/code/xdoctest/dev/demo_dynamic_analysis.py --analysis=auto - - xdoctest ~/code/xdoctest/dev/demo_dynamic_analysis.py --analysis=dynamic - - xdoctest ~/code/xdoctest/dev/demo_dynamic_analysis.py --xdoc-force-dynamic -""" - - -def func() -> None: - r''' Dynamic doctest - >>> %s - %s - ''' - return - -func.__doc__ %= ('print(1)', '1') diff --git a/dev/interactive_embed_tests.py b/dev/devcheck/interactive_embed_tests.py similarity index 71% rename from dev/interactive_embed_tests.py rename to dev/devcheck/interactive_embed_tests.py index 6b843d4d..a86140a8 100644 --- a/dev/interactive_embed_tests.py +++ b/dev/devcheck/interactive_embed_tests.py @@ -3,7 +3,7 @@ def interative_test_xdev_embed(): """ CommandLine: - xdoctest -m dev/interactive_embed_tests.py interative_test_xdev_embed + xdoctest -m dev/demo/interactive_embed_tests.py interative_test_xdev_embed Example: >>> interative_test_xdev_embed() @@ -16,7 +16,7 @@ def interative_test_xdev_embed(): def interative_test_ipdb_embed(): """ CommandLine: - xdoctest -m dev/interactive_embed_tests.py interative_test_ipdb_embed + xdoctest -m dev/demo/interactive_embed_tests.py interative_test_ipdb_embed Example: >>> interative_test_ipdb_embed() diff --git a/dev/port_ubelt_utils.py b/dev/maintain/port_ubelt_utils.py similarity index 97% rename from dev/port_ubelt_utils.py rename to dev/maintain/port_ubelt_utils.py index a89c5e6a..a45682ab 100644 --- a/dev/port_ubelt_utils.py +++ b/dev/maintain/port_ubelt_utils.py @@ -65,6 +65,6 @@ def _autogen_xdoctest_utils(): if __name__ == '__main__': """ CommandLine: - python ~/code/xdoctest/dev/port_ubelt_utils.py + python ~/code/xdoctest/dev/maintain/port_ubelt_utils.py """ _autogen_xdoctest_utils() diff --git a/src/xdoctest/__init__.py b/src/xdoctest/__init__.py index 00372fd9..c3ddfbfc 100644 --- a/src/xdoctest/__init__.py +++ b/src/xdoctest/__init__.py @@ -313,7 +313,7 @@ def fib(n): mkinit xdoctest --nomods ''' -__version__ = '1.1.4' +__version__ = '1.1.5' # Expose only select submodules diff --git a/src/xdoctest/__main__.py b/src/xdoctest/__main__.py index 3688b47c..4b9e2399 100644 --- a/src/xdoctest/__main__.py +++ b/src/xdoctest/__main__.py @@ -27,8 +27,8 @@ def main(argv=None): argv = sys.argv version_info = { - 'version': xdoctest.__version__, 'sys_version': sys.version, + 'version': xdoctest.__version__, } if '--version' in argv: @@ -36,8 +36,9 @@ def main(argv=None): return 0 if '--version-info' in argv: - for key, value in sorted(version_info.items()): - print('{} = {}'.format(key, value)) + print('sys_version = {}'.format(version_info['sys_version'])) + print('file = {}'.format(__file__)) + print('version = {}'.format(version_info['version'])) return 0 import argparse @@ -69,7 +70,8 @@ class RawDescriptionDefaultsHelpFormatter( If the `--command` key / value pair is unspecified, the first positional argument is used as the command. ''')) - parser.add_argument('--version', action='store_true', help='Display version info and quit') + parser.add_argument('--version', action='store_true', help='Display version and quit') + parser.add_argument('--version-info', action='store_true', help='Display version and other info and quit') # The bulk of the argparse CLI is defined in the doctest example from xdoctest import doctest_example diff --git a/src/xdoctest/utils/util_import.py b/src/xdoctest/utils/util_import.py index 10c99a5b..24b47e32 100644 --- a/src/xdoctest/utils/util_import.py +++ b/src/xdoctest/utils/util_import.py @@ -62,6 +62,37 @@ def _importlib_import_modpath(modpath): # nocover return module +def _pkgutil_modname_to_modpath(modname): # nocover + """ + faster version of :func:`_syspath_modname_to_modpath` using builtin python + mechanisms, but unfortunately it doesn't play nice with pytest. + + Note: + pkgutil.find_loader is deprecated in 3.12 and removed in 3.14 + + Args: + modname (str): the module name. + + Example: + >>> # xdoctest: +SKIP + >>> modname = 'xdoctest.static_analysis' + >>> _pkgutil_modname_to_modpath(modname) + ...static_analysis.py + >>> # xdoctest: +REQUIRES(CPython) + >>> _pkgutil_modname_to_modpath('_ctypes') + ..._ctypes... + + Ignore: + >>> _pkgutil_modname_to_modpath('cv2') + """ + import pkgutil + loader = pkgutil.find_loader(modname) + if loader is None: + raise Exception('No module named {} in the PYTHONPATH'.format(modname)) + modpath = loader.get_filename().replace('.pyc', '.py') + return modpath + + class PythonPathContext(object): """ Context for temporarily adding a dir to the PYTHONPATH. @@ -180,9 +211,10 @@ def _custom_import_modpath(modpath, index=-1): with PythonPathContext(dpath, index=index): module = import_module_from_name(modname) except Exception as ex: # nocover - msg_parts = [ - 'ERROR: Failed to import modname={} with modpath={}'.format( - modname, modpath) + msg_parts = [( + 'ERROR: Failed to import modname={} with modpath={} and ' + 'sys.path modified with {} at index={}').format( + modname, modpath, repr(dpath), index) ] msg_parts.append('Caused by: {}'.format(repr(ex))) raise RuntimeError('\n'.join(msg_parts)) @@ -292,13 +324,14 @@ def import_module_from_path(modpath, index=-1): >>> assert module.testvar == 1 Example: - >>> # xdoctest: +SKIP("ubelt dependency") >>> import pytest + >>> # xdoctest: +SKIP("ubelt dependency") >>> with pytest.raises(IOError): >>> ub.import_module_from_path('does-not-exist') >>> with pytest.raises(IOError): >>> ub.import_module_from_path('does-not-exist.zip/') """ + modpath = os.fspath(modpath) if not os.path.exists(modpath): import re import zipimport @@ -452,6 +485,13 @@ def _static_parse(varname, fpath): """ Statically parse the a constant variable from a python file + Args: + varname (str): variable name to extract + fpath (str | PathLike): path to python file to parse + + Returns: + Any: the static value + Example: >>> # xdoctest: +SKIP("ubelt dependency") >>> dpath = ub.Path.appdir('tests/import/staticparse').ensuredir() @@ -475,6 +515,10 @@ def _static_parse(varname, fpath): >>> with pytest.raises(AttributeError): >>> fpath.write_text('a = list(range(10))') >>> assert _static_parse('c', fpath) is None + >>> if sys.version_info[0:2] >= (3, 6): + >>> # Test with type annotations + >>> fpath.write_text('b: int = 10') + >>> assert _static_parse('b', fpath) == 10 """ import ast @@ -487,9 +531,16 @@ def _static_parse(varname, fpath): class StaticVisitor(ast.NodeVisitor): def visit_Assign(self, node): for target in node.targets: - if getattr(target, 'id', None) == varname: + target_id = getattr(target, 'id', None) + if target_id == varname: self.static_value = _parse_static_node_value(node.value) + def visit_AnnAssign(self, node): + target = node.target + target_id = getattr(target, 'id', None) + if target_id == varname: + self.static_value = _parse_static_node_value(node.value) + visitor = StaticVisitor() visitor.visit(pt) try: @@ -743,51 +794,6 @@ def check_dpath(dpath): return found_modpath -def _importlib_modname_to_modpath(modname): # nocover - import importlib.util - spec = importlib.util.find_spec(modname) - print(f'spec={spec}') - modpath = spec.origin.replace('.pyc', '.py') - return modpath - - -def _pkgutil_modname_to_modpath(modname): # nocover - """ - faster version of :func:`_syspath_modname_to_modpath` using builtin python - mechanisms, but unfortunately it doesn't play nice with pytest. - - Note: - pkgutil.find_loader is deprecated in 3.12 and removed in 3.14 - - Args: - modname (str): the module name. - - Example: - >>> # xdoctest: +SKIP - >>> modname = 'xdoctest.static_analysis' - >>> _pkgutil_modname_to_modpath(modname) - ...static_analysis.py - >>> # xdoctest: +REQUIRES(CPython) - >>> _pkgutil_modname_to_modpath('_ctypes') - ..._ctypes... - - Ignore: - >>> _pkgutil_modname_to_modpath('cv2') - """ - import pkgutil - loader = pkgutil.find_loader(modname) - if loader is None: - raise Exception('No module named {} in the PYTHONPATH'.format(modname)) - try: - modpath = loader.get_filename().replace('.pyc', '.py') - except Exception: - print('Issue in _pkgutil_modname_to_modpath') - print(f'loader = {loader!r}') - print(f'modname = {modname!r}') - raise - return modpath - - def modname_to_modpath(modname, hide_init=True, hide_main=False, sys_path=None): """ Finds the path to a python module from its name.