From 56f0b4ab0dc9b09a27cf2dbbcf48f88997c3a70c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 8 Apr 2026 23:04:30 +0000 Subject: [PATCH 1/3] Rewrite pyeval for Python 3 with pyproject.toml and flake.nix Agent-Logs-Url: https://github.com/nejucomo/pyeval/sessions/6d2e325f-0af3-4755-87d1-6641530d9553 Co-authored-by: nejucomo <480497+nejucomo@users.noreply.github.com> --- .gitignore | 26 ++++++++ .hgignore | 8 --- MANIFEST.in | 1 - bin/pyeval | 3 - flake.nix | 59 +++++++++++++++++++ lib/pyeval/doc/encoding.txt | 15 ----- pyproject.toml | 20 +++++++ run_tests.sh | 43 -------------- setup.py | 24 -------- {lib => src}/pyeval/__init__.py | 0 {lib => src}/pyeval/autoimporter.py | 30 +++------- {lib => src}/pyeval/copyright.py | 0 {lib => src}/pyeval/display.py | 5 +- {lib => src}/pyeval/doc/AutoImporter.txt | 0 src/pyeval/doc/encoding.txt | 13 ++++ {lib => src}/pyeval/doc/examples.txt | 0 {lib => src}/pyeval/doc/help.txt | 5 +- {lib => src}/pyeval/doc/intro.txt | 0 {lib => src}/pyeval/doc/magic.txt | 2 +- {lib => src}/pyeval/doc/upgrading.txt | 4 +- {lib => src}/pyeval/eval.py | 11 ++-- {lib => src}/pyeval/help.py | 20 ++++--- {lib => src}/pyeval/indentation.py | 8 +-- {lib => src}/pyeval/magic/__init__.py | 0 {lib => src}/pyeval/magic/functions.py | 28 ++++----- {lib => src}/pyeval/magic/scope.py | 35 ++++------- {lib => src}/pyeval/magic/variables.py | 6 +- {lib => src}/pyeval/main.py | 3 +- {lib => src}/pyeval/monkeypatch.py | 0 {lib => src}/pyeval/tests/__init__.py | 0 {lib => src}/pyeval/tests/fakeio.py | 23 ++++---- .../pyeval/tests/test_autoimporter.py | 32 ++++++---- {lib => src}/pyeval/tests/test_display.py | 6 +- {lib => src}/pyeval/tests/test_eval.py | 19 +++--- {lib => src}/pyeval/tests/test_help.py | 15 ++--- {lib => src}/pyeval/tests/test_indentation.py | 3 +- {lib => src}/pyeval/tests/test_magicscope.py | 25 +++----- {lib => src}/pyeval/tests/test_main.py | 5 +- 38 files changed, 237 insertions(+), 260 deletions(-) create mode 100644 .gitignore delete mode 100644 .hgignore delete mode 100644 MANIFEST.in delete mode 100755 bin/pyeval create mode 100644 flake.nix delete mode 100644 lib/pyeval/doc/encoding.txt create mode 100644 pyproject.toml delete mode 100755 run_tests.sh delete mode 100755 setup.py rename {lib => src}/pyeval/__init__.py (100%) rename {lib => src}/pyeval/autoimporter.py (75%) rename {lib => src}/pyeval/copyright.py (100%) rename {lib => src}/pyeval/display.py (58%) rename {lib => src}/pyeval/doc/AutoImporter.txt (100%) create mode 100644 src/pyeval/doc/encoding.txt rename {lib => src}/pyeval/doc/examples.txt (100%) rename {lib => src}/pyeval/doc/help.txt (88%) rename {lib => src}/pyeval/doc/intro.txt (100%) rename {lib => src}/pyeval/doc/magic.txt (93%) rename {lib => src}/pyeval/doc/upgrading.txt (95%) rename {lib => src}/pyeval/eval.py (92%) rename {lib => src}/pyeval/help.py (77%) rename {lib => src}/pyeval/indentation.py (81%) rename {lib => src}/pyeval/magic/__init__.py (100%) rename {lib => src}/pyeval/magic/functions.py (75%) rename {lib => src}/pyeval/magic/scope.py (73%) mode change 100755 => 100644 rename {lib => src}/pyeval/magic/variables.py (90%) rename {lib => src}/pyeval/main.py (85%) rename {lib => src}/pyeval/monkeypatch.py (100%) rename {lib => src}/pyeval/tests/__init__.py (100%) rename {lib => src}/pyeval/tests/fakeio.py (61%) rename {lib => src}/pyeval/tests/test_autoimporter.py (71%) rename {lib => src}/pyeval/tests/test_display.py (82%) rename {lib => src}/pyeval/tests/test_eval.py (90%) rename {lib => src}/pyeval/tests/test_help.py (89%) rename {lib => src}/pyeval/tests/test_indentation.py (92%) rename {lib => src}/pyeval/tests/test_magicscope.py (74%) rename {lib => src}/pyeval/tests/test_main.py (87%) diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..35c3868 --- /dev/null +++ b/.gitignore @@ -0,0 +1,26 @@ +# Python +__pycache__/ +*.py[cod] +*.egg-info/ +dist/ +build/ +*.egg + +# Virtual environments +.venv/ +venv/ +env/ + +# Nix +result +result-* + +# pytest +.pytest_cache/ +.coverage +htmlcov/ + +# Editor +.idea/ +.vscode/ +*.swp diff --git a/.hgignore b/.hgignore deleted file mode 100644 index 755d0f8..0000000 --- a/.hgignore +++ /dev/null @@ -1,8 +0,0 @@ -^MANIFEST$ -^build/ -^dist/ -^\.coverage -^htmlcov/ -^_trial_temp/ -^.*\.pyc$ -^lib/pyeval.egg-info diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index eb762f3..0000000 --- a/MANIFEST.in +++ /dev/null @@ -1 +0,0 @@ -include COPYING diff --git a/bin/pyeval b/bin/pyeval deleted file mode 100755 index d7d23d6..0000000 --- a/bin/pyeval +++ /dev/null @@ -1,3 +0,0 @@ -#!/usr/bin/env python -from pyeval.main import main -main() diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..b5488e9 --- /dev/null +++ b/flake.nix @@ -0,0 +1,59 @@ +{ + description = "pyeval - Conveniently evaluate Python expressions from the shell"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + flake-utils.url = "github:numtide/flake-utils"; + }; + + outputs = { self, nixpkgs, flake-utils }: + flake-utils.lib.eachDefaultSystem (system: + let + pkgs = nixpkgs.legacyPackages.${system}; + python = pkgs.python3; + + pyeval = python.pkgs.buildPythonPackage { + pname = "pyeval"; + version = "0.3.0"; + format = "pyproject"; + + src = ./.; + + nativeBuildInputs = [ + python.pkgs.setuptools + ]; + + meta = with pkgs.lib; { + description = "Conveniently evaluate Python expressions from the shell"; + license = licenses.gpl3; + mainProgram = "pyeval"; + }; + }; + in { + packages = { + default = pyeval; + pyeval = pyeval; + }; + + apps.default = flake-utils.lib.mkApp { + drv = pyeval; + name = "pyeval"; + }; + + devShells.default = pkgs.mkShell { + packages = [ + (python.withPackages (ps: with ps; [ + pip + pytest + setuptools + ])) + ]; + + shellHook = '' + export PYTHONPATH="$PWD/src:$PYTHONPATH" + echo "pyeval dev shell — run 'pytest src/pyeval/tests' to test" + ''; + }; + } + ); +} diff --git a/lib/pyeval/doc/encoding.txt b/lib/pyeval/doc/encoding.txt deleted file mode 100644 index 49dfcec..0000000 --- a/lib/pyeval/doc/encoding.txt +++ /dev/null @@ -1,15 +0,0 @@ -The p() magic function encodes unicode arguments using an encoding -selected in this order, where the first defined value is used: - -1. sys.stdout.encoding -2. The substring of the LC_CTYPE environment variable after the rightmost '.' -3. UTF-8 - -The first works when the stdout is connected to a terminal. When stdout -is not connected to a terminal, such as when pyeval is used in a shell -pipeline, the second will hopefully choose the user's preferred encoding. - -There is a potential for LC_CTYPE to be a valid setting for the user's -locale, but to be invalid as a python encoding specifier. - -Several other display functions, such as sh() rely on p(). diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..c8f7132 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,20 @@ +[build-system] +requires = ["setuptools>=61"] +build-backend = "setuptools.build_meta" + +[project] +name = "pyeval" +version = "0.3.0" +description = "Conveniently evaluate Python expressions from the shell" +authors = [{name = "Nathan Wilcox", email = "nejucomo@gmail.com"}] +license = {text = "GPLv3"} +requires-python = ">=3.8" + +[project.scripts] +pyeval = "pyeval.main:main" + +[tool.setuptools.packages.find] +where = ["src"] + +[tool.setuptools.package-data] +pyeval = ["doc/*.txt"] diff --git a/run_tests.sh b/run_tests.sh deleted file mode 100755 index 93ffb0a..0000000 --- a/run_tests.sh +++ /dev/null @@ -1,43 +0,0 @@ -#!/bin/bash - -PYTHONPATH=".:./lib:$PYTHONPATH" - - -echo '=== pyflakes ===' -pyflakes ./lib/pyeval || exit $? -echo 'pyflakes completed.' - - -echo -e '\n=== Running unittests ===' -TRIAL=$(which trial) - -if ! [ -x "$TRIAL" ]; -then - echo 'Could not find trial; it is in the Twisted package.' - exit -1 -fi - - -coverage run --branch "$TRIAL" ./lib/pyeval -STATUS=$? - -echo -e '\n--- Generating Coverage Report ---' -coverage html --include='lib/pyeval/*' - -echo 'Report generated.' - -[ "$STATUS" -eq 0 ] || exit $STATUS - - -echo -e '\n=== Smoke Test ===' -./bin/pyeval 'os._exit(0)' -STATUS=$? - -if [ "$STATUS" -eq 0 ] -then - echo 'Smoke-test Succeeded.' -else - echo 'Smoke-test FAILED.' -fi - -exit "$STATUS" diff --git a/setup.py b/setup.py deleted file mode 100755 index 61ee0a4..0000000 --- a/setup.py +++ /dev/null @@ -1,24 +0,0 @@ -#!/usr/bin/env python - -import os, sys -from setuptools import setup, find_packages - - -if 'upload' in sys.argv: - if '--sign' not in sys.argv and sys.argv[1:] != ['upload', '--help']: - raise SystemExit('Refusing to upload unsigned packages.') - - -setup(name='pyeval', - description='Conveniently evaluate expressions from the shell', - version='0.2.1a0', - author='Nathan Wilcox', - author_email='nejucomo@gmail.com', - license='GPLv3', - url='https://bitbucket.org/nejucomo/pyeval', - - scripts=[os.path.join('bin', 'pyeval')], - packages=find_packages('lib'), - package_dir={'': 'lib'}, - package_data={'pyeval': ['doc/*.txt']}, - ) diff --git a/lib/pyeval/__init__.py b/src/pyeval/__init__.py similarity index 100% rename from lib/pyeval/__init__.py rename to src/pyeval/__init__.py diff --git a/lib/pyeval/autoimporter.py b/src/pyeval/autoimporter.py similarity index 75% rename from lib/pyeval/autoimporter.py rename to src/pyeval/autoimporter.py index 26126fb..9a7d9ed 100644 --- a/lib/pyeval/autoimporter.py +++ b/src/pyeval/autoimporter.py @@ -5,13 +5,12 @@ from weakref import WeakKeyDictionary +class AutoImporter(object): -class AutoImporter (object): + class Proxy(object): + pass # BaseClass exposed so client code can use isinstance(x, AutoImporter.Proxy) - class Proxy (object): - pass # BaseClass exposed so client code can use isinstance(x, AutoImport.Proxy) - - class ModInfo (object): + class ModInfo(object): def __init__(self, mod): self.mod = mod @@ -21,26 +20,15 @@ def name(self): @property def path(self): - try: - path = self.mod.__file__ - except AttributeError: - return None - else: - if path.endswith('.pyc'): - path = path[:-1] - - return path - + return getattr(self.mod, '__file__', None) def __init__(self): self._proxyInfo = WeakKeyDictionary() def proxyImport(self, name): - mod = __import__(name) - for name in name.split('.')[1:]: - mod = getattr(mod, name) - + for part in name.split('.')[1:]: + mod = getattr(mod, part) return self._proxyWrap(mod) def mod(self, proxy): @@ -64,7 +52,7 @@ def _proxyWrap(self, mod): ai = self modinfo = ai.ModInfo(mod) - class BoundProxy (ai.Proxy): + class BoundProxy(ai.Proxy): def __repr__(_): modrepr = repr(mod) assert modrepr.startswith('<') and modrepr.endswith('>'), modrepr @@ -73,7 +61,7 @@ def __repr__(_): def __getattribute__(_, name): try: x = getattr(mod, name) - except AttributeError, outerError: + except AttributeError as outerError: try: x = ai.proxyImport(modinfo.name + '.' + name) except ImportError: diff --git a/lib/pyeval/copyright.py b/src/pyeval/copyright.py similarity index 100% rename from lib/pyeval/copyright.py rename to src/pyeval/copyright.py diff --git a/lib/pyeval/display.py b/src/pyeval/display.py similarity index 58% rename from lib/pyeval/display.py rename to src/pyeval/display.py index 8d636c9..df5f6e7 100644 --- a/lib/pyeval/display.py +++ b/src/pyeval/display.py @@ -16,8 +16,5 @@ def displayPretty(obj): def getEncoding(): - # NOTE: I do not know how well this will work in practice: - # If sys.stdout.encoding is not set (because stdout is not a terminal), - # we use LC_CTYPE *anyway*. return (getattr(sys.stdout, 'encoding', None) - or os.environ.get('LC_CTYPE', 'UTF-8').split( '.', 1 )[-1]) + or os.environ.get('LC_CTYPE', 'UTF-8').split('.', 1)[-1]) diff --git a/lib/pyeval/doc/AutoImporter.txt b/src/pyeval/doc/AutoImporter.txt similarity index 100% rename from lib/pyeval/doc/AutoImporter.txt rename to src/pyeval/doc/AutoImporter.txt diff --git a/src/pyeval/doc/encoding.txt b/src/pyeval/doc/encoding.txt new file mode 100644 index 0000000..6572ef9 --- /dev/null +++ b/src/pyeval/doc/encoding.txt @@ -0,0 +1,13 @@ +In Python 3, text encoding is handled transparently by the standard +library. The 'p()' magic function uses Python's built-in print(), +which respects sys.stdout.encoding automatically. + +The 'encoding' magic variable still provides access to the detected +encoding, selected in this order: + +1. sys.stdout.encoding +2. The substring of the LC_CTYPE environment variable after the rightmost '.' +3. UTF-8 + +Several display functions, such as sh() and p(), rely on Python's +standard print() which handles unicode natively. diff --git a/lib/pyeval/doc/examples.txt b/src/pyeval/doc/examples.txt similarity index 100% rename from lib/pyeval/doc/examples.txt rename to src/pyeval/doc/examples.txt diff --git a/lib/pyeval/doc/help.txt b/src/pyeval/doc/help.txt similarity index 88% rename from lib/pyeval/doc/help.txt rename to src/pyeval/doc/help.txt index ed1b2e5..675f162 100644 --- a/lib/pyeval/doc/help.txt +++ b/src/pyeval/doc/help.txt @@ -11,17 +11,16 @@ argument. For example: To examine source docstrings any object, such as a class, pass it to help as a function: - $ pyeval 'help(BaseHTTPServer.HTTPServer)' + $ pyeval 'help(http.server.HTTPServer)' ... There is a special case for AutoImporter module proxies, so that the original module is passed on to the pydoc help system: - $ pyeval 'help(BaseHTTPServer)' + $ pyeval 'help(http.server)' ... To use the interactive help system of pydoc, call it directly: $ pyeval 'pydoc.help()' ... - diff --git a/lib/pyeval/doc/intro.txt b/src/pyeval/doc/intro.txt similarity index 100% rename from lib/pyeval/doc/intro.txt rename to src/pyeval/doc/intro.txt diff --git a/lib/pyeval/doc/magic.txt b/src/pyeval/doc/magic.txt similarity index 93% rename from lib/pyeval/doc/magic.txt rename to src/pyeval/doc/magic.txt index 232be06..a2f1a87 100644 --- a/lib/pyeval/doc/magic.txt +++ b/src/pyeval/doc/magic.txt @@ -5,7 +5,7 @@ of dict. A MagicScope has two special properties: * unbound references are passed through to a "fallthrough" handler. The standard fallthrough handler attempts to find the reference in -__builtin__, or if it is not there, it delegates to an AutoImporter +builtins, or if it is not there, it delegates to an AutoImporter instance. For more detail about the AutoImporter mechanism, run: $ pyeval help AutoImporter diff --git a/lib/pyeval/doc/upgrading.txt b/src/pyeval/doc/upgrading.txt similarity index 95% rename from lib/pyeval/doc/upgrading.txt rename to src/pyeval/doc/upgrading.txt index 5ce93da..da74eb4 100644 --- a/lib/pyeval/doc/upgrading.txt +++ b/src/pyeval/doc/upgrading.txt @@ -46,9 +46,9 @@ is not defined in the math module: $ pyeval 'type(math._ai_mod)' Traceback (most recent call last): ... - AttributeError: 'module' object has no attribute '_ai_mod' + AttributeError: ... Now to access the proxied module, call 'ai.mod': $ pyeval 'type(ai.mod(math))' - + diff --git a/lib/pyeval/eval.py b/src/pyeval/eval.py similarity index 92% rename from lib/pyeval/eval.py rename to src/pyeval/eval.py index 1d019b0..5fe3383 100644 --- a/lib/pyeval/eval.py +++ b/src/pyeval/eval.py @@ -1,7 +1,7 @@ __all__ = ['pyeval', 'pyevalAndDisplay', 'buildStandardMagicScope'] -import __builtin__ +import builtins import sys from pyeval.autoimporter import AutoImporter @@ -11,19 +11,18 @@ from pyeval.monkeypatch import patch - def pyevalAndDisplay(expr, *args, **kw): """ Evaluate expr and args, then display the result with sys.displayhook. If the displayhook keyword is given and None, sys.displayhook is not modified. Otherwise it is temporarily assigned to sys.displayhook - and restored before returning. It not given, it defaults to + and restored before returning. If not given, it defaults to pyeval.display.displayPretty. """ displayhook = kw.pop('displayhook', displayPretty) if len(kw) > 0: - raise TypeError('pyevalAndDisplay() got unexpected keywords: %r' % (kw.keys(),)) + raise TypeError('pyevalAndDisplay() got unexpected keywords: %r' % (list(kw.keys()),)) if displayhook is None: displayhook = sys.displayhook @@ -43,7 +42,7 @@ def buildStandardMagicScope(argStrs, autoimporter=None): def fallthrough(key): try: - return getattr(__builtin__, key) + return getattr(builtins, key) except AttributeError: return autoimporter.proxyImport(key) @@ -72,5 +71,3 @@ def ai(_): scope.registerMagicFunction(getattr(functions, name)) return scope - - diff --git a/lib/pyeval/help.py b/src/pyeval/help.py similarity index 77% rename from lib/pyeval/help.py rename to src/pyeval/help.py index 2f5e033..6d413ad 100644 --- a/lib/pyeval/help.py +++ b/src/pyeval/help.py @@ -4,11 +4,11 @@ import os -import pkg_resources +import importlib.resources from pyeval.indentation import dedent, indent -class HelpBrowser (object): +class HelpBrowser(object): def __init__(self, scope, delegate=help): """The constructor allows dependency injection for unittests.""" @@ -18,11 +18,12 @@ def __init__(self, scope, delegate=help): self._delegate = delegate self._topics = {} - for topicfile in pkg_resources.resource_listdir(__name__, 'doc'): - if topicfile.endswith('.txt'): - topicname = topicfile[:-4] - resource = os.path.join('doc', topicname + '.txt') - self._topics[topicname] = pkg_resources.resource_string(__name__, resource) + pkg_ref = importlib.resources.files(__name__).joinpath('doc') + for resource in pkg_ref.iterdir(): + name = resource.name + if name.endswith('.txt'): + topicname = name[:-4] + self._topics[topicname] = resource.read_text(encoding='utf-8') self._topics['variables'] = self._createVariablesTopic() @@ -43,7 +44,7 @@ def getTopicText(self, topicname): return '%s\n%s\n\n%s\n' % (header, '=' * len(header), topictext) def renderTopic(self, topicname): - print self.getTopicText(topicname) + print(self.getTopicText(topicname)) def render(self): args = self._scope['args'] @@ -52,11 +53,12 @@ def render(self): try: [topicname] = args except ValueError: - raise SystemExit('Too many args for help.') # FIXME + raise SystemExit('Too many args for help.') # FIXME self.renderTopic(topicname) _NoArgSentinel = object() + def __call__(self, obj=_NoArgSentinel): if obj is self._NoArgSentinel: self.renderTopic('help') diff --git a/lib/pyeval/indentation.py b/src/pyeval/indentation.py similarity index 81% rename from lib/pyeval/indentation.py rename to src/pyeval/indentation.py index ade792c..c7bc02c 100644 --- a/lib/pyeval/indentation.py +++ b/src/pyeval/indentation.py @@ -14,7 +14,7 @@ def dedent(text): dedentedlines = [] for indented in indentedlines: - assert indented == '' or indented[:indent].strip() == '', `indented` + assert indented == '' or indented[:indent].strip() == '', repr(indented) dedentedlines.append(indented[indent:]) return '\n'.join(dedentedlines) + '\n' @@ -22,6 +22,6 @@ def dedent(text): def indent(text, amount=2): """Indent text by amount spaces.""" - indent = ' ' * amount - separator = '\n' + indent - return indent + separator.join(text.rstrip().split('\n')) + '\n' + ind = ' ' * amount + separator = '\n' + ind + return ind + separator.join(text.rstrip().split('\n')) + '\n' diff --git a/lib/pyeval/magic/__init__.py b/src/pyeval/magic/__init__.py similarity index 100% rename from lib/pyeval/magic/__init__.py rename to src/pyeval/magic/__init__.py diff --git a/lib/pyeval/magic/functions.py b/src/pyeval/magic/functions.py similarity index 75% rename from lib/pyeval/magic/functions.py rename to src/pyeval/magic/functions.py index 022d8e6..db1a14e 100644 --- a/lib/pyeval/magic/functions.py +++ b/src/pyeval/magic/functions.py @@ -21,19 +21,17 @@ def pp(_, *a, **kw): def p(scope, x): r""" - A wrapper around the print statement. Use this if you want - to avoid pretty printed results: + A wrapper around print(). Use this if you want to avoid pretty + printed results: $ pyeval 'p({}.get("nothing"))' None - $ pyeval 'range(123)' - [0, - 1, - ... + $ pyeval 'list(range(5))' + [0, 1, 2, 3, 4] - $ pyeval 'p(range(123))' - [0, 1, ... + $ pyeval 'p(list(range(5)))' + [0, 1, 2, 3, 4] Also, it allows you to print strings directly: @@ -41,7 +39,7 @@ def p(scope, x): x y z - Note, it's possible to display unicode this way, using the detected encoding: + Note, it's possible to display unicode this way: $ pyeval 'p(u"\u2606")' ☆ @@ -51,11 +49,7 @@ def p(scope, x): $ pyeval help encoding ... """ - - if type(x) is unicode: - x = x.encode(scope['encoding']) - - print x + print(x) def sh(scope, obj): @@ -68,7 +62,7 @@ def sh(scope, obj): For each item in the iterable: - 3. Convert the item to unicode as: unicode(item) + 3. Convert the item to a string as: str(item) 4. Print the result with p(). @@ -92,11 +86,11 @@ def sh(scope, obj): return it = [obj] - if type(obj) not in (str, unicode): + if not isinstance(obj, str): try: it = iter(obj) except TypeError: pass for elem in it: - p(unicode(elem)) + p(str(elem)) diff --git a/lib/pyeval/magic/scope.py b/src/pyeval/magic/scope.py old mode 100755 new mode 100644 similarity index 73% rename from lib/pyeval/magic/scope.py rename to src/pyeval/magic/scope.py index 7cf2218..070b067 --- a/lib/pyeval/magic/scope.py +++ b/src/pyeval/magic/scope.py @@ -4,15 +4,13 @@ from functools import wraps - -class MagicScope (dict): +class MagicScope(dict): def __init__(self, fallthrough): assert callable(fallthrough) self._fallthrough = fallthrough self._magic = {} - # Explicit magic interface: def registerMagic(self, f, name=None, doc=None): @@ -22,14 +20,12 @@ def registerMagic(self, f, name=None, doc=None): if doc is None: doc = f.__doc__ - self.pop(name, None) # Override any previous definitions. + self.pop(name, None) # Override any previous definitions. self._magic[name] = (f, doc) - def registerMagicConstant(self, value, name, doc): self.registerMagic(lambda _: value, name, doc) - def registerMagicFunction(self, f): @wraps(f) @@ -41,17 +37,14 @@ def wrapped(*a, **kw): self.registerMagic(magicWrapper) - def getMagicDocs(self): - return sorted( [ (k, doc) for (k, (f, doc)) in self._magic.iteritems() ] ) - + return sorted([(k, doc) for (k, (f, doc)) in self._magic.items()]) # dict interface: def __repr__(self): # Explicitly override dict.__repr__ to prevent evaluating magic values: return '<%s %r>' % (type(self).__name__, sorted(self.keys())) - def __getitem__(self, key): (method, _) = self._magic.get(key, (None, None)) @@ -61,39 +54,31 @@ def __getitem__(self, key): if method is None: try: return self._fallthrough(key) - except Exception: # Dangerous! + except Exception: # Dangerous! raise NameError(key) else: value = self[key] = method(self) return value - def __len__(self): return len(self.keys()) - - def iterkeys(self): + def __iter__(self): visited = set() - for key in self._magic.iterkeys(): + for key in self._magic.keys(): visited.add(key) yield key - for key in dict.iterkeys(self): + for key in dict.keys(self): if key not in visited: yield key - def iteritems(self): - return ( (k, self[k]) for k in self.iterkeys() ) - - def itervalues(self): - return ( v for (k, v) in self.iteritems() ) - def keys(self): - return list(self.iterkeys()) + return list(self.__iter__()) def items(self): - return list(self.iteritems()) + return [(k, self[k]) for k in self.__iter__()] def values(self): - return list(self.itervalues()) + return [v for (k, v) in self.items()] diff --git a/lib/pyeval/magic/variables.py b/src/pyeval/magic/variables.py similarity index 90% rename from lib/pyeval/magic/variables.py rename to src/pyeval/magic/variables.py index 37939e3..5660e14 100644 --- a/lib/pyeval/magic/variables.py +++ b/src/pyeval/magic/variables.py @@ -21,7 +21,7 @@ def scope(scope): def encoding(scope): - r"""The detected encoding used by p() and other magic functions.""" + r"""The detected encoding used by the display functions.""" return getEncoding() @@ -75,7 +75,7 @@ def lines(scope): The list of stripped standard input lines. Defined as: '[ l.strip() for l in scope['rlines'] ]' """ - return [ l.strip() for l in scope['rlines'] ] + return [l.strip() for l in scope['rlines']] def ilines(_): @@ -83,4 +83,4 @@ def ilines(_): A line iterator over stripped lines from stdin. Defined as: '( l.strip() for l in sys.stdin )' """ - return ( l.strip() for l in sys.stdin ) + return (l.strip() for l in sys.stdin) diff --git a/lib/pyeval/main.py b/src/pyeval/main.py similarity index 85% rename from lib/pyeval/main.py rename to src/pyeval/main.py index 418b4db..ab5bc07 100644 --- a/lib/pyeval/main.py +++ b/src/pyeval/main.py @@ -5,10 +5,9 @@ from pyeval.eval import pyevalAndDisplay -def main(args = sys.argv[1:]): +def main(args=sys.argv[1:]): if len(args) == 0 or args[0] in ['-h', '--help']: args = ['help'] pyevalAndDisplay(*args) - diff --git a/lib/pyeval/monkeypatch.py b/src/pyeval/monkeypatch.py similarity index 100% rename from lib/pyeval/monkeypatch.py rename to src/pyeval/monkeypatch.py diff --git a/lib/pyeval/tests/__init__.py b/src/pyeval/tests/__init__.py similarity index 100% rename from lib/pyeval/tests/__init__.py rename to src/pyeval/tests/__init__.py diff --git a/lib/pyeval/tests/fakeio.py b/src/pyeval/tests/fakeio.py similarity index 61% rename from lib/pyeval/tests/fakeio.py rename to src/pyeval/tests/fakeio.py index 1f00e98..fc58776 100644 --- a/lib/pyeval/tests/fakeio.py +++ b/src/pyeval/tests/fakeio.py @@ -3,13 +3,12 @@ import re import sys -from cStringIO import StringIO +from io import StringIO - -class FakeIO (object): - def __init__(self, inbytes=''): - self._inbytes = inbytes +class FakeIO(object): + def __init__(self, intext=''): + self._intext = intext def __enter__(self): self._realout = sys.stdout @@ -18,7 +17,7 @@ def __enter__(self): self.fakeout = sys.stdout = StringIO() self.fakeerr = sys.stderr = StringIO() - self.fakein = sys.stdin = StringIO(self._inbytes) + self.fakein = sys.stdin = StringIO(self._intext) return self @@ -36,7 +35,11 @@ def checkLiteral(self, testcase, expectedOut, expectedError): def checkRegexp(self, testcase, expectedOut, expectedError): (output, error) = self.getOutputs() - testcase.assertRegexpMatches(output, expectedOut) - testcase.assertRegexpMatches(error, expectedError) - - + if isinstance(expectedOut, str) and not expectedOut: + testcase.assertEqual('', output) + else: + testcase.assertRegex(output, expectedOut) + if isinstance(expectedError, str) and not expectedError: + testcase.assertEqual('', error) + else: + testcase.assertRegex(error, expectedError) diff --git a/lib/pyeval/tests/test_autoimporter.py b/src/pyeval/tests/test_autoimporter.py similarity index 71% rename from lib/pyeval/tests/test_autoimporter.py rename to src/pyeval/tests/test_autoimporter.py index 0108e6f..738ab82 100644 --- a/lib/pyeval/tests/test_autoimporter.py +++ b/src/pyeval/tests/test_autoimporter.py @@ -7,8 +7,7 @@ from pyeval.autoimporter import AutoImporter - -class AutoImporterTests (unittest.TestCase): +class AutoImporterTests(unittest.TestCase): def setUp(self): self.ai = AutoImporter() self.parent = self.ai.proxyImport('logging') @@ -36,19 +35,30 @@ def test_name(self): self.assertEqual('logging.handlers', self.ai.name(self.child)) def test_path(self): - def getsrc(m): - path = m.__file__ - assert path.endswith('.pyc') - return path[:-1] - - self.assertEqual(getsrc(logging), self.ai.path(self.parent)) - self.assertEqual(getsrc(logging.handlers), self.ai.path(self.child)) + for (proxy, mod) in [(self.parent, logging), (self.child, handlers)]: + path = self.ai.path(proxy) + self.assertIsNotNone(path) + self.assertTrue(path.endswith('.py'), 'Expected .py path, got: %r' % path) + self.assertEqual(mod.__file__, path) def test_pathNone(self): self.assertIsNone(self.ai.path(self.ai.proxyImport('sys'))) def test_pathDotSO(self): - self.assertRegexpMatches(self.ai.path(self.ai.proxyImport('_struct')), '\.so$') + # Find a C extension module with a .so file + so_module = None + for candidate in ['netifaces', '_cffi_backend', 'apt_inst', '_datetime']: + try: + mod = __import__(candidate) + path = getattr(mod, '__file__', None) + if path and path.endswith('.so'): + so_module = candidate + break + except ImportError: + pass + if so_module is None: + self.skipTest('No .so extension module available in this environment') + self.assertRegex(self.ai.path(self.ai.proxyImport(so_module)), r'\.so$') def test_nameTypeError(self): self.assertRaises(TypeError, self.ai.name, 42) @@ -78,5 +88,3 @@ def test_AttributeError(self): self.assertRaises(AttributeError, getattr, proxy, 'WOMBATS!') except ImportError: self.fail('A missing attribute on an AutoImporter resulted in an ImportError.') - - diff --git a/lib/pyeval/tests/test_display.py b/src/pyeval/tests/test_display.py similarity index 82% rename from lib/pyeval/tests/test_display.py rename to src/pyeval/tests/test_display.py index 5b44985..e6134aa 100644 --- a/lib/pyeval/tests/test_display.py +++ b/src/pyeval/tests/test_display.py @@ -1,12 +1,12 @@ import unittest import pprint -from cStringIO import StringIO +from io import StringIO from pyeval.display import displayPretty from pyeval.tests.fakeio import FakeIO -class displayPrettyTests (unittest.TestCase): +class displayPrettyTests(unittest.TestCase): """displayPretty should behave like standard sys.displayhook, except pformat is used.""" def test_displayNone(self): @@ -18,7 +18,7 @@ def test_displayNone(self): fio.checkLiteral(self, '', '') def test_displayValues(self): - for value in [42, "banana", range(1024), vars()]: + for value in [42, "banana", list(range(1024)), vars()]: f = StringIO() pprint.pprint(value, f) expected = f.getvalue() diff --git a/lib/pyeval/tests/test_eval.py b/src/pyeval/tests/test_eval.py similarity index 90% rename from lib/pyeval/tests/test_eval.py rename to src/pyeval/tests/test_eval.py index d5470f5..bd7ee31 100644 --- a/lib/pyeval/tests/test_eval.py +++ b/src/pyeval/tests/test_eval.py @@ -9,8 +9,7 @@ from pyeval.tests.fakeio import FakeIO - -class pyevalTests (unittest.TestCase): +class pyevalTests(unittest.TestCase): def test_autoimportTopLevel(self): self.assertIs(math, pyeval('ai.mod(math)')) @@ -21,14 +20,14 @@ def test_args(self): self.assertRaises(NameError, pyeval, 'a2', 'x', 'y') def test_autoimportSubmodule(self): - proxy = pyeval('cStringIO') + proxy = pyeval('logging.handlers') self.assertIsInstance(proxy, AutoImporter.Proxy) def test_unboundRaisesNameError(self): self.assertRaises(NameError, pyeval, 'BLORK_IS_NOT_BOUND') -class pyevalAndDisplayTests (unittest.TestCase): +class pyevalAndDisplayTests(unittest.TestCase): def _test_pead(self, expected, args): displays = [] @@ -59,11 +58,11 @@ def test_unexpectedKeyword(self): self.assertRaises(TypeError, pyevalAndDisplay, '42', wombat='monkey') -class StandardMagicScopeTests (unittest.TestCase): +class StandardMagicScopeTests(unittest.TestCase): def setUp(self): self.imports = [] - class FakeAutoImporter (object): + class FakeAutoImporter(object): def proxyImport(_, name): self.imports.append(name) @@ -73,7 +72,7 @@ def proxyImport(_, name): self.scope = buildStandardMagicScope(self.args, self.fakeai) def test_conciseBindings(self): - # The standard MagicScope delegates to __builtin__ in its + # The standard MagicScope delegates to builtins in its # fallthrough, rather than dumping all builtins into the dict. # This keeps the output short and relevant of: # $ pyeval 'dir()' @@ -84,7 +83,7 @@ def test_inputCaching(self): rawin = 'foo\nbar\n\n' stripin = rawin.strip() rlines = rawin.split('\n') - lines = [ l.strip() for l in rlines ] + lines = [l.strip() for l in rlines] with FakeIO(rawin): for i in range(2): @@ -134,8 +133,8 @@ def test_output(arg, expected): test_output('foo', 'foo\n') test_output(42, '42\n') test_output(['foo', 42], 'foo\n42\n') - test_output( ( x for x in ['foo', 42] ), 'foo\n42\n') - test_output( {'x': 'xylophone', 'y': 'yam'}, 'y\nx\n') + test_output((x for x in ['foo', 42]), 'foo\n42\n') + test_output({'x': 'xylophone', 'y': 'yam'}, 'x\ny\n') def test_magicFunctionNamesMatchBinding(self): for (name, _) in self.scope.getMagicDocs(): diff --git a/lib/pyeval/tests/test_help.py b/src/pyeval/tests/test_help.py similarity index 89% rename from lib/pyeval/tests/test_help.py rename to src/pyeval/tests/test_help.py index eac3875..9bf30db 100644 --- a/lib/pyeval/tests/test_help.py +++ b/src/pyeval/tests/test_help.py @@ -10,8 +10,7 @@ from pyeval.tests.fakeio import FakeIO - -class HelpBrowserTests (unittest.TestCase): +class HelpBrowserTests(unittest.TestCase): def setUp(self): self.delegateCalls = [] self.help = HelpBrowser(buildStandardMagicScope([]), self.delegateCalls.append) @@ -23,15 +22,13 @@ def test_autoImporter(self): self.assertEqual([sys], self.delegateCalls) - -class DocExampleVerificationTests (unittest.TestCase): +class DocExampleVerificationTests(unittest.TestCase): IndentRgx = re.compile(r'^ .*?$', re.MULTILINE) InvocationRgx = re.compile(r"^ \$") PyevalInvocationRgx = re.compile( r"^ \$ (echo (?P-e )?'(?P.*?)' \| )?pyeval (?P('.*?'|\S+)) ?(?P.*?)$") - def _parseEntries(self, text): entry = None for m in self.IndentRgx.finditer(text): @@ -55,21 +52,19 @@ def _parseEntries(self, text): expr = expr[1:-1] entry = (expr, args, teststdin, []) else: - # This is an non-tested example, such as a non-call to pyeval. - #print 'DEBUG: Skipping non-pyeval shell example: %r' % (match,) + # This is a non-tested example, such as a non-call to pyeval. entry = (None, None, None, []) if entry is not None and entry[0] is not None: yield entry - def test_docs(self): hb = HelpBrowser(buildStandardMagicScope([])) count = 0 - topics = [ (topic, hb.getTopicText(topic)) for topic in hb.getTopics() ] + topics = [(topic, hb.getTopicText(topic)) for topic in hb.getTopics()] for (topicname, helptext) in topics: for (expr, args, inputText, outlines) in self._parseEntries(helptext): @@ -98,7 +93,7 @@ def test_docs(self): else: fio.checkRegexp(self, expectedRgx, '^$') - except Exception, e: + except Exception as e: e.args += ('In topic %r' % (topicname,), 'In EXPR %r' % (expr,), ) diff --git a/lib/pyeval/tests/test_indentation.py b/src/pyeval/tests/test_indentation.py similarity index 92% rename from lib/pyeval/tests/test_indentation.py rename to src/pyeval/tests/test_indentation.py index add1af3..46d83da 100644 --- a/lib/pyeval/tests/test_indentation.py +++ b/src/pyeval/tests/test_indentation.py @@ -3,8 +3,7 @@ from pyeval.indentation import dedent, indent - -class indentationTests (unittest.TestCase): +class indentationTests(unittest.TestCase): def test_dedentAndIndent(self): x = """ diff --git a/lib/pyeval/tests/test_magicscope.py b/src/pyeval/tests/test_magicscope.py similarity index 74% rename from lib/pyeval/tests/test_magicscope.py rename to src/pyeval/tests/test_magicscope.py index 9076520..e9da74e 100644 --- a/lib/pyeval/tests/test_magicscope.py +++ b/src/pyeval/tests/test_magicscope.py @@ -3,8 +3,7 @@ from pyeval.magic.scope import MagicScope - -class MagicScopeTests (unittest.TestCase): +class MagicScopeTests(unittest.TestCase): def setUp(self): self.caught = [] self.scope = MagicScope(self.caught.append) @@ -62,7 +61,7 @@ def y(scope): self.assertIsInstance(v, str) -class MagicScopeDictInterfaceTests (unittest.TestCase): +class MagicScopeDictInterfaceTests(unittest.TestCase): def setUp(self): def failIfCalled(key): @@ -80,12 +79,12 @@ def y(scope): def test___repr__(self): # The repr should *not* delegate to dict, because some magic # values trigger IO: - self.assertRegexpMatches(repr(self.scope), r'$') + self.assertRegex(repr(self.scope), r'$') def _testInvariantEquality(self, expected, f, *args): self.assertEqual(expected, f(*args)) - self.scope['y'] # Resolve the magic variable. + self.scope['y'] # Resolve the magic variable. # Repeat the invariant: self.assertEqual(expected, f(*args)) @@ -94,20 +93,10 @@ def test___len__(self): self._testInvariantEquality(2, len, self.scope) def test_keys(self): - self._testInvariantEquality(['x', 'y'], lambda : sorted(self.scope.keys())) + self._testInvariantEquality(['x', 'y'], lambda: sorted(self.scope.keys())) def test_values(self): - self._testInvariantEquality([0, 1], lambda : sorted(self.scope.values())) + self._testInvariantEquality([0, 1], lambda: sorted(self.scope.values())) def test_items(self): - self._testInvariantEquality([('x', 0), ('y', 1)], lambda : sorted(self.scope.items())) - - def test_iterkeys(self): - self._testInvariantEquality(['x', 'y'], lambda : sorted(self.scope.iterkeys())) - - def test_itervalues(self): - self._testInvariantEquality([0, 1], lambda : sorted(self.scope.itervalues())) - - def test_iteritems(self): - self._testInvariantEquality([('x', 0), ('y', 1)], lambda : sorted(self.scope.iteritems())) - + self._testInvariantEquality([('x', 0), ('y', 1)], lambda: sorted(self.scope.items())) diff --git a/lib/pyeval/tests/test_main.py b/src/pyeval/tests/test_main.py similarity index 87% rename from lib/pyeval/tests/test_main.py rename to src/pyeval/tests/test_main.py index d1f14e1..11f87b5 100644 --- a/lib/pyeval/tests/test_main.py +++ b/src/pyeval/tests/test_main.py @@ -4,7 +4,7 @@ from pyeval.tests.fakeio import FakeIO -class pyevalTests (unittest.TestCase): +class pyevalTests(unittest.TestCase): def test_fortytwo(self): fio = FakeIO() @@ -33,5 +33,4 @@ def test_scopeRepr(self): with fio: main(['scope']) - fio.checkRegexp(self, '^$', '^$') - + fio.checkRegexp(self, '^$', '^$') From a7381cd1796fe8c232450add4a4122a09aac526f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 8 Apr 2026 23:06:41 +0000 Subject: [PATCH 2/3] Fix encoding docstring formatting and clarify description Agent-Logs-Url: https://github.com/nejucomo/pyeval/sessions/6d2e325f-0af3-4755-87d1-6641530d9553 Co-authored-by: nejucomo <480497+nejucomo@users.noreply.github.com> --- src/pyeval/magic/variables.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/pyeval/magic/variables.py b/src/pyeval/magic/variables.py index 5660e14..cc8d740 100644 --- a/src/pyeval/magic/variables.py +++ b/src/pyeval/magic/variables.py @@ -21,7 +21,13 @@ def scope(scope): def encoding(scope): - r"""The detected encoding used by the display functions.""" + r""" + The detected encoding. + + In Python 3, print() handles encoding automatically. This variable + provides the detected encoding for informational use. Detection order: + sys.stdout.encoding, LC_CTYPE env var suffix, or UTF-8. + """ return getEncoding() From f4736a28057d0a0e240ed864ca9346bbb317abbf Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 9 Apr 2026 00:25:54 +0000 Subject: [PATCH 3/3] Add GitHub CI workflow with tests, coverage, and doc tests Agent-Logs-Url: https://github.com/nejucomo/pyeval/sessions/aeefd62e-621a-4174-918d-1f9fa220eb05 Co-authored-by: nejucomo <480497+nejucomo@users.noreply.github.com> --- .github/workflows/ci.yml | 52 ++++++++++++++++++++++++++++++++++++++++ pyproject.toml | 14 +++++++++++ 2 files changed, 66 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..5328c33 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,52 @@ +name: CI + +on: + push: + branches: ["master", "main"] + pull_request: + branches: ["master", "main"] + +jobs: + test: + name: "Python ${{ matrix.python-version }}" + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: ["3.8", "3.10", "3.12"] + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Install package and test dependencies + run: | + python -m pip install --upgrade pip + pip install .[test] + + - name: Run unit tests and doc tests with coverage + run: | + pytest \ + --cov=pyeval \ + --cov-report=term-missing \ + --cov-report=xml:coverage.xml \ + --cov-report=html:coverage-html \ + src/pyeval/tests/ + + - name: Upload coverage HTML report + if: matrix.python-version == '3.12' + uses: actions/upload-artifact@v4 + with: + name: coverage-report + path: coverage-html/ + + - name: Upload coverage XML (for external tools) + if: matrix.python-version == '3.12' + uses: actions/upload-artifact@v4 + with: + name: coverage-xml + path: coverage.xml diff --git a/pyproject.toml b/pyproject.toml index c8f7132..6bf9eb4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,8 +13,22 @@ requires-python = ">=3.8" [project.scripts] pyeval = "pyeval.main:main" +[project.optional-dependencies] +test = ["pytest", "pytest-cov"] + [tool.setuptools.packages.find] where = ["src"] [tool.setuptools.package-data] pyeval = ["doc/*.txt"] + +[tool.pytest.ini_options] +addopts = "--tb=short" +testpaths = ["src/pyeval/tests"] + +[tool.coverage.run] +branch = true +source = ["pyeval"] + +[tool.coverage.report] +show_missing = true