Skip to content

Commit

Permalink
Merge pull request #2 from mdmintz/python-3.12-compatibility
Browse files Browse the repository at this point in the history
Python 3.12 compatibility
  • Loading branch information
mdmintz committed May 2, 2023
2 parents 4e0d2bb + 2fa074b commit 6514ba8
Show file tree
Hide file tree
Showing 5 changed files with 138 additions and 14 deletions.
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,15 @@

``pynose`` is an updated version of ``nose``, originally made by Jason Pellerin.

This version of ``nose`` is compatible with Python 3.6+ (including 3.11 & up).
This version of ``nose`` is compatible with Python 3.6+ (including 3.12 & up).

Changes in ``pynose`` from legacy ``nose`` include:
* Fixes "AttributeError: module 'collections' has no attribute 'Callable'."
* Fixes all ``flake8`` issues from the original ``nose``.
* The default logging level now hides "debug" logs.
* Fixes "ImportError: cannot import name '_TextTestResult' from 'unittest'."
* Fixes "RuntimeWarning: TestResult has no addDuration method."
* Replaces the ``imp`` library with the newer ``importlib`` library.
* The default logging level now hides "debug" logs for less noise.
* Adds ``--co`` as a shortcut to using ``--collect-only``.

--------
Expand Down
2 changes: 1 addition & 1 deletion nose/__version__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
# pynose nose package
__version__ = "1.4.2"
__version__ = "1.4.3"
4 changes: 4 additions & 0 deletions nose/case.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,9 @@ def run(self, result):
finally:
self.afterTest(result)

def addDuration(*args, **kwargs):
pass

def runTest(self, result):
"""Run the test. Plugins may alter the test by returning a
value from prepareTestCase. The value must be callable and
Expand All @@ -128,6 +131,7 @@ def runTest(self, result):
plug_test = self.config.plugins.prepareTestCase(self)
if plug_test is not None:
test = plug_test
result.addDuration = self.addDuration
test(result)

def shortDescription(self):
Expand Down
136 changes: 126 additions & 10 deletions nose/importer.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,124 @@
import logging
import os
import sys
import importlib.machinery
import importlib.util
import tokenize
from nose.config import Config
from imp import find_module, load_module, acquire_lock, release_lock
from importlib import _imp
from importlib._bootstrap import _ERR_MSG, _builtin_from_name

acquire_lock = _imp.acquire_lock
is_builtin = _imp.is_builtin
init_frozen = _imp.init_frozen
is_frozen = _imp.is_frozen
release_lock = _imp.release_lock
SEARCH_ERROR = 0
PY_SOURCE = 1
PY_COMPILED = 2
C_EXTENSION = 3
PY_RESOURCE = 4
PKG_DIRECTORY = 5
C_BUILTIN = 6
PY_FROZEN = 7
PY_CODERESOURCE = 8
IMP_HOOK = 9


def get_suffixes():
extensions = [
(s, 'rb', C_EXTENSION) for s in importlib.machinery.EXTENSION_SUFFIXES
]
source = [
(s, 'r', PY_SOURCE) for s in importlib.machinery.SOURCE_SUFFIXES
]
bytecode = [
(s, 'rb', PY_COMPILED) for s in importlib.machinery.BYTECODE_SUFFIXES
]
return extensions + source + bytecode


def init_builtin(name):
try:
return _builtin_from_name(name)
except ImportError:
return None


def load_package(name, path):
if os.path.isdir(path):
extensions = (
importlib.machinery.SOURCE_SUFFIXES[:]
+ importlib.machinery.BYTECODE_SUFFIXES[:]
)
for extension in extensions:
init_path = os.path.join(path, '__init__' + extension)
if os.path.exists(init_path):
path = init_path
break
else:
raise ValueError('{!r} is not a package'.format(path))
spec = importlib.util.spec_from_file_location(
name, path, submodule_search_locations=[]
)
sys.modules[name] = importlib.util.module_from_spec(spec)
spec.loader.exec_module(sys.modules[name])
return sys.modules[name]


def find_module(name, path=None):
"""Search for a module.
If path is omitted or None, search for a built-in, frozen or special
module and continue search in sys.path. The module name cannot
contain '.'; to search for a submodule of a package, pass the
submodule name and the package's __path__."""
if is_builtin(name):
return None, None, ('', '', C_BUILTIN)
elif is_frozen(name):
return None, None, ('', '', PY_FROZEN)

# find_spec(fullname, path=None, target=None)
spec = importlib.machinery.PathFinder().find_spec(
fullname=name, path=path
)
if spec is None:
raise ImportError(_ERR_MSG.format(name), name=name)

# RETURN (file, file_path, desc=(suffix, mode, type_))
if os.path.splitext(os.path.basename(spec.origin))[0] == '__init__':
return None, os.path.dirname(spec.origin), ('', '', PKG_DIRECTORY)
for suffix, mode, type_ in get_suffixes():
if spec.origin.endswith(suffix):
break
else:
suffix = '.py'
mode = 'r'
type_ = PY_SOURCE

encoding = None
if 'b' not in mode:
with open(spec.origin, 'rb') as file:
encoding = tokenize.detect_encoding(file.readline)[0]
file = open(spec.origin, mode, encoding=encoding)
return file, spec.origin, (suffix, mode, type_)


def load_module(name, file, filename, details):
"""Load a module, given information returned by find_module().
The module name must include the full package name, if any."""
suffix, mode, type_ = details
if type_ == PKG_DIRECTORY:
return load_package(name, filename)
elif type_ == C_BUILTIN:
return init_builtin(name)
elif type_ == PY_FROZEN:
return init_frozen(name)
spec = importlib.util.spec_from_file_location(name, filename)
mod = importlib.util.module_from_spec(spec)
sys.modules[name] = mod
spec.loader.exec_module(mod)
return mod


log = logging.getLogger(__name__)
try:
Expand All @@ -19,9 +135,9 @@ def _samefile(src, dst):


class Importer(object):
"""An importer class that does only path-specific imports. That
is, the given module is not searched for on sys.path, but only at
the path or in the directory specified."""
"""An importer class that does only path-specific imports.
That is, the given module is not searched for on sys.path,
but only at the path or in the directory specified."""
def __init__(self, config=None):
if config is None:
config = Config()
Expand All @@ -41,8 +157,8 @@ def importFromPath(self, path, fqname):
return self.importFromDir(dir_path, fqname)

def importFromDir(self, dir, fqname):
"""Import a module *only* from path, ignoring sys.path and
reloading if the version in sys.modules is not the one we want."""
"""Import a module *only* from path, ignoring sys.path and reloading
if the version in sys.modules is not the one we want."""
dir = os.path.normpath(os.path.abspath(dir))
log.debug("Import %s from %s", fqname, dir)
if fqname == '__main__':
Expand Down Expand Up @@ -88,8 +204,8 @@ def importFromDir(self, dir, fqname):

def _dirname_if_file(self, filename):
# We only take the dirname if we have a path to a non-dir,
# because taking the dirname of a symlink to a directory does not
# give the actual directory parent.
# because taking the dirname of a symlink to a directory
# does not give the actual directory parent.
if os.path.isdir(filename):
return filename
else:
Expand All @@ -115,8 +231,8 @@ def sameModule(self, mod, filename):


def add_path(path, config=None):
"""Ensure that the path, or the root of the current package (if
path is in a package), is in sys.path."""
"""Ensure that the path, or the root of the current
package (if path is in a package), is in sys.path."""
log.debug('Add path %s' % path)
if not path:
return []
Expand Down
3 changes: 2 additions & 1 deletion nose/result.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
provide support for error classes (such as the builtin skip and deprecated
classes), and hooks for plugins to take over or extend reporting."""
import logging
from unittest import _TextTestResult
from unittest import TextTestResult as _TextTestResult
from nose.config import Config
from nose.util import isclass, ln as _ln

Expand All @@ -32,6 +32,7 @@ def __init__(self, stream, descriptions, verbosity, config=None,
config = Config()
self.config = config
_TextTestResult.__init__(self, stream, descriptions, verbosity)
_TextTestResult.addDuration = None

def addSkip(self, test, reason):
from nose.plugins.skip import SkipTest
Expand Down

0 comments on commit 6514ba8

Please sign in to comment.