diff --git a/.github/workflows/pylint.yml b/.github/workflows/pylint.yml index 9c226a1c4..dce3b0b86 100644 --- a/.github/workflows/pylint.yml +++ b/.github/workflows/pylint.yml @@ -13,35 +13,33 @@ jobs: strategy: matrix: - os: [ubuntu-latest, windows-latest] - python-version: [3.7, 3.8, 3.9] + os: [ ubuntu-latest, windows-latest ] + python-version: [ 3.9, '3.10', 3.11, 3.12 ] exclude: - - os: windows-latest - python-version: 3.8 - os: windows-latest python-version: 3.9 steps: - - uses: actions/checkout@v2 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 - with: - python-version: ${{ matrix.python-version }} - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install pylint - pip install -r requirements.txt - pip install -r development.txt - pip install ntc_templates==1.4.1 - pip install textfsm==0.4.1 - pip install . - - - name: Run black tool - run: | - pip install -U black; - black --check --diff --exclude="docs|build|tests|samples" . - - - name: Run unit tests - run: | - nosetests -v --with-coverage --cover-package=jnpr.junos --cover-inclusive -a unit + - uses: actions/checkout@v2 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install pylint + pip install -r requirements.txt + pip install -r development.txt + pip install ntc_templates==1.4.1 + pip install textfsm==0.4.1 + pip install . + + - name: Run black tool + run: | + pip install -U black; + black --check --diff --exclude="docs|build|tests|samples" . + + - name: Run unit tests + run: | + nose2 --with-coverage -vvv tests.unit diff --git a/.gitignore b/.gitignore index bcf37e4f8..ea8ceba36 100644 --- a/.gitignore +++ b/.gitignore @@ -17,6 +17,7 @@ develop-eggs .installed.cfg lib64 env +.venv # Documentation docs/_build @@ -42,6 +43,8 @@ MANIFEST # Testing .tox +foo_* +testfile # Vagrant .vagrant diff --git a/README.md b/README.md index 954cc7c2d..bc1f48c9e 100644 --- a/README.md +++ b/README.md @@ -66,7 +66,7 @@ _Junos PyEZ_ is designed to provide the same capabilities as a user would have o ## PIP -Installation requires Python >=3.5 and associated `pip` tool +Installation requires Python >=3.8 and associated `pip` tool pip install junos-eznc diff --git a/development.txt b/development.txt index 93175ef40..f47e21ca4 100644 --- a/development.txt +++ b/development.txt @@ -2,7 +2,7 @@ coverage # http://nedbatchelder.com/code/coverage/ mock # http://www.voidspace.org.uk/python/mock/ -nose # http://nose.readthedocs.org/en/latest/ +nose2 # https://docs.nose2.io/en/latest/ pep8 # https://github.com/jcrocholl/pep8 pyflakes # https://launchpad.net/pyflakes coveralls # https://coveralls.io/ diff --git a/docs/conf.py b/docs/conf.py index 8cc077b1f..15c8c742e 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -19,22 +19,22 @@ # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. -sys.path.insert(0, os.path.abspath('../lib')) +sys.path.insert(0, os.path.abspath("../lib")) # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. -#needs_sphinx = '1.0' +# needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ - 'sphinx.ext.autodoc', - 'sphinx.ext.viewcode', - 'sphinx.ext.coverage', - 'sphinx.ext.todo', - 'sphinx.ext.intersphinx', + "sphinx.ext.autodoc", + "sphinx.ext.viewcode", + "sphinx.ext.coverage", + "sphinx.ext.todo", + "sphinx.ext.intersphinx", ] @@ -47,22 +47,24 @@ def skip(app, what, name, obj, skip, options): def setup(app): app.connect("autodoc-skip-member", skip) + # Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +templates_path = ["_templates"] # The suffix of source filenames. -source_suffix = '.rst' +source_suffix = ".rst" # The encoding of source files. -#source_encoding = 'utf-8-sig' +# source_encoding = 'utf-8-sig' # The master toctree document. -master_doc = 'index' +master_doc = "index" # General information about the project. import jnpr.junos -project = u'Junos PyEZ' -copyright = u'2017, Juniper Networks, Inc.' + +project = "Junos PyEZ" +copyright = "2017, Juniper Networks, Inc." # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the @@ -75,41 +77,41 @@ def setup(app): # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. -#language = None +# language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: -#today = '' +# today = '' # Else, today_fmt is used as the format for a strftime call. -#today_fmt = '%B %d, %Y' +# today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. -exclude_patterns = ['_build'] +exclude_patterns = ["_build"] # The reST default role (used for this markup: `text`) to use for all # documents. -#default_role = None +# default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. -#add_function_parentheses = True +# add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). -#add_module_names = True +# add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. -#show_authors = False +# show_authors = False # The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' +pygments_style = "sphinx" # A list of ignored prefixes for module index sorting. -#modindex_common_prefix = [] +# modindex_common_prefix = [] # If true, keep warnings as "system message" paragraphs in the built documents. -#keep_warnings = False +# keep_warnings = False # Produce todo output todo_include_todos = True @@ -119,21 +121,19 @@ def setup(app): # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -html_theme = 'bootstrap' +html_theme = "bootstrap" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. html_theme_options = { - 'bootswatch_theme': "spacelab", - 'navbar_sidebarrel': False, - 'navbar_site_name': "Module", - 'navbar_pagenav_name': "Classes", - 'source_link_position': "footer", - 'navbar_links': [ - ("Wiki", - "https://techwiki.juniper.net/Automation_Scripting/Junos_PyEZ", - True), + "bootswatch_theme": "spacelab", + "navbar_sidebarrel": False, + "navbar_site_name": "Module", + "navbar_pagenav_name": "Classes", + "source_link_position": "footer", + "navbar_links": [ + ("Wiki", "https://techwiki.juniper.net/Automation_Scripting/Junos_PyEZ", True), ("Forum", "http://groups.google.com/group/junos-python-ez", True), ], } @@ -143,78 +143,78 @@ def setup(app): # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". -#html_title = None +# html_title = None # A shorter title for the navigation bar. Default is the same as html_title. -#html_short_title = None +# html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. -html_logo = 'juniper.png' +html_logo = "juniper.png" # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. -#html_favicon = None +# html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] +html_static_path = ["_static"] # Add any extra paths that contain custom files (such as robots.txt or # .htaccess) here, relative to this directory. These files are copied # directly to the root of the documentation. -#html_extra_path = [] +# html_extra_path = [] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. -#html_last_updated_fmt = '%b %d, %Y' +# html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. -#html_use_smartypants = True +# html_use_smartypants = True # Custom sidebar templates, maps document names to template names. html_sidebars = { - '**': [ - 'globaltoc.html', - 'localtoc.html', + "**": [ + "globaltoc.html", + "localtoc.html", ] } # Additional templates that should be rendered to pages, maps page names to # template names. -#html_additional_pages = {} +# html_additional_pages = {} # If false, no module index is generated. -#html_domain_indices = True +# html_domain_indices = True # If false, no index is generated. -#html_use_index = True +# html_use_index = True # If true, the index is split into individual pages for each letter. -#html_split_index = False +# html_split_index = False # If true, links to the reST sources are added to the pages. -#html_show_sourcelink = True +# html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. -#html_show_sphinx = True +# html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -#html_show_copyright = True +# html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. -#html_use_opensearch = '' +# html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). -#html_file_suffix = None +# html_file_suffix = None # Output file base name for HTML help builder. -htmlhelp_basename = 'JunosPyEZdoc' +htmlhelp_basename = "JunosPyEZdoc" # -- Options for LaTeX output --------------------------------------------- @@ -222,10 +222,8 @@ def setup(app): latex_elements = { # The paper size ('letterpaper' or 'a4paper'). #'papersize': 'letterpaper', - # The font size ('10pt', '11pt' or '12pt'). #'pointsize': '10pt', - # Additional stuff for the LaTeX preamble. #'preamble': '', } @@ -234,29 +232,34 @@ def setup(app): # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ - ('index', 'JunosPyEZ.tex', u'Junos PyEZ Documentation', - u'Juniper Networks, Inc.', 'manual'), + ( + "index", + "JunosPyEZ.tex", + "Junos PyEZ Documentation", + "Juniper Networks, Inc.", + "manual", + ), ] # The name of an image file (relative to this directory) to place at the top of # the title page. -#latex_logo = None +# latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. -#latex_use_parts = False +# latex_use_parts = False # If true, show page references after internal links. -#latex_show_pagerefs = False +# latex_show_pagerefs = False # If true, show URL addresses after external links. -#latex_show_urls = False +# latex_show_urls = False # Documents to append as an appendix to all manuals. -#latex_appendices = [] +# latex_appendices = [] # If false, no module index is generated. -#latex_domain_indices = True +# latex_domain_indices = True # -- Options for manual page output --------------------------------------- @@ -264,12 +267,11 @@ def setup(app): # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ - ('index', 'junospyez', u'Junos PyEZ Documentation', - [u'Juniper Networks, Inc.'], 1) + ("index", "junospyez", "Junos PyEZ Documentation", ["Juniper Networks, Inc."], 1) ] # If true, show URL addresses after external links. -#man_show_urls = False +# man_show_urls = False # -- Options for Texinfo output ------------------------------------------- @@ -278,90 +280,95 @@ def setup(app): # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - ('index', 'JunosPyEZ', u'Junos PyEZ Documentation', - u'Juniper Networks, Inc.', 'JunosPyEZ', - 'One line description of project.', - 'Miscellaneous'), + ( + "index", + "JunosPyEZ", + "Junos PyEZ Documentation", + "Juniper Networks, Inc.", + "JunosPyEZ", + "One line description of project.", + "Miscellaneous", + ), ] # Documents to append as an appendix to all manuals. -#texinfo_appendices = [] +# texinfo_appendices = [] # If false, no module index is generated. -#texinfo_domain_indices = True +# texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. -#texinfo_show_urls = 'footnote' +# texinfo_show_urls = 'footnote' # If true, do not generate a @detailmenu in the "Top" node's menu. -#texinfo_no_detailmenu = False +# texinfo_no_detailmenu = False # -- Options for Epub output ---------------------------------------------- # Bibliographic Dublin Core info. -epub_title = u'Junos PyEZ' -epub_author = u'Juniper Networks, Inc.' -epub_publisher = u'Juniper Networks, Inc.' -epub_copyright = u'2014, Juniper Networks, Inc.' +epub_title = "Junos PyEZ" +epub_author = "Juniper Networks, Inc." +epub_publisher = "Juniper Networks, Inc." +epub_copyright = "2014, Juniper Networks, Inc." # The basename for the epub file. It defaults to the project name. -#epub_basename = u'Junos PyEZ' +# epub_basename = u'Junos PyEZ' # The HTML theme for the epub output. Since the default themes are not # optimized for small screen space, using the same theme for HTML and epub # output is usually not wise. This defaults to 'epub', a theme designed to # save visual space. -#epub_theme = 'epub' +# epub_theme = 'epub' # The language of the text. It defaults to the language option # or en if the language is not set. -#epub_language = '' +# epub_language = '' # The scheme of the identifier. Typical schemes are ISBN or URL. -#epub_scheme = '' +# epub_scheme = '' # The unique identifier of the text. This can be a ISBN number # or the project homepage. -#epub_identifier = '' +# epub_identifier = '' # A unique identification for the text. -#epub_uid = '' +# epub_uid = '' # A tuple containing the cover image and cover page html template filenames. -#epub_cover = () +# epub_cover = () # A sequence of (type, uri, title) tuples for the guide element of content.opf. -#epub_guide = () +# epub_guide = () # HTML files that should be inserted before the pages created by sphinx. # The format is a list of tuples containing the path and title. -#epub_pre_files = [] +# epub_pre_files = [] # HTML files shat should be inserted after the pages created by sphinx. # The format is a list of tuples containing the path and title. -#epub_post_files = [] +# epub_post_files = [] # A list of files that should not be packed into the epub file. -epub_exclude_files = ['search.html'] +epub_exclude_files = ["search.html"] # The depth of the table of contents in toc.ncx. -#epub_tocdepth = 3 +# epub_tocdepth = 3 # Allow duplicate toc entries. -#epub_tocdup = True +# epub_tocdup = True # Choose between 'default' and 'includehidden'. -#epub_tocscope = 'default' +# epub_tocscope = 'default' # Fix unsupported image types using the PIL. -#epub_fix_images = False +# epub_fix_images = False # Scale large images. -#epub_max_image_width = 0 +# epub_max_image_width = 0 # How to display URL addresses: 'footnote', 'no', or 'inline'. -#epub_show_urls = 'inline' +# epub_show_urls = 'inline' # If false, no index is generated. -#epub_use_index = True +# epub_use_index = True diff --git a/lib/jnpr/junos/__init__.py b/lib/jnpr/junos/__init__.py index b815f2fea..3a155b8a6 100644 --- a/lib/jnpr/junos/__init__.py +++ b/lib/jnpr/junos/__init__.py @@ -43,3 +43,7 @@ def emit(self, record): __version__ = get_versions()["version"] del get_versions + +from . import _version + +__version__ = _version.get_versions()["version"] diff --git a/lib/jnpr/junos/_version.py b/lib/jnpr/junos/_version.py index 180cba836..c83f6966d 100644 --- a/lib/jnpr/junos/_version.py +++ b/lib/jnpr/junos/_version.py @@ -4,8 +4,9 @@ # directories (produced by setup.py build) will contain a much shorter file # that just contains the computed version number. -# This file is released into the public domain. Generated by -# versioneer-0.18 (https://github.com/warner/python-versioneer) +# This file is released into the public domain. +# Generated by versioneer-0.29 +# https://github.com/python-versioneer/python-versioneer """Git implementation of _version.py.""" @@ -14,9 +15,11 @@ import re import subprocess import sys +from typing import Any, Callable, Dict, List, Optional, Tuple +import functools -def get_keywords(): +def get_keywords() -> Dict[str, str]: """Get the keywords needed to look up the version information.""" # these strings will be replaced by git during git-archive. # setup.py/versioneer.py will grep for the variable names, so they must @@ -32,8 +35,15 @@ def get_keywords(): class VersioneerConfig: """Container for Versioneer configuration parameters.""" + VCS: str + style: str + tag_prefix: str + parentdir_prefix: str + versionfile_source: str + verbose: bool -def get_config(): + +def get_config() -> VersioneerConfig: """Create, populate and return the VersioneerConfig() object.""" # these strings are filled in when 'setup.py versioneer' creates # _version.py @@ -51,14 +61,14 @@ class NotThisMethod(Exception): """Exception raised if a method is not valid for the current scenario.""" -LONG_VERSION_PY = {} -HANDLERS = {} +LONG_VERSION_PY: Dict[str, str] = {} +HANDLERS: Dict[str, Dict[str, Callable]] = {} -def register_vcs_handler(vcs, method): # decorator - """Decorator to mark a method as the handler for a particular VCS.""" +def register_vcs_handler(vcs: str, method: str) -> Callable: # decorator + """Create decorator to mark a method as the handler of a VCS.""" - def decorate(f): + def decorate(f: Callable) -> Callable: """Store f in HANDLERS[vcs][method].""" if vcs not in HANDLERS: HANDLERS[vcs] = {} @@ -68,24 +78,39 @@ def decorate(f): return decorate -def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False, env=None): +def run_command( + commands: List[str], + args: List[str], + cwd: Optional[str] = None, + verbose: bool = False, + hide_stderr: bool = False, + env: Optional[Dict[str, str]] = None, +) -> Tuple[Optional[str], Optional[int]]: """Call the given command(s).""" assert isinstance(commands, list) - p = None - for c in commands: + process = None + + popen_kwargs: Dict[str, Any] = {} + if sys.platform == "win32": + # This hides the console window if pythonw.exe is used + startupinfo = subprocess.STARTUPINFO() + startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW + popen_kwargs["startupinfo"] = startupinfo + + for command in commands: try: - dispcmd = str([c] + args) + dispcmd = str([command] + args) # remember shell=False, so use git.cmd on windows, not just git - p = subprocess.Popen( - [c] + args, + process = subprocess.Popen( + [command] + args, cwd=cwd, env=env, stdout=subprocess.PIPE, stderr=(subprocess.PIPE if hide_stderr else None), + **popen_kwargs, ) break - except EnvironmentError: - e = sys.exc_info()[1] + except OSError as e: if e.errno == errno.ENOENT: continue if verbose: @@ -96,18 +121,20 @@ def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False, env= if verbose: print("unable to find command, tried %s" % (commands,)) return None, None - stdout = p.communicate()[0].strip() - if sys.version_info[0] >= 3: - stdout = stdout.decode() - if p.returncode != 0: + stdout = process.communicate()[0].strip().decode() + if process.returncode != 0: if verbose: print("unable to run %s (error)" % dispcmd) print("stdout was %s" % stdout) - return None, p.returncode - return stdout, p.returncode + return None, process.returncode + return stdout, process.returncode -def versions_from_parentdir(parentdir_prefix, root, verbose): +def versions_from_parentdir( + parentdir_prefix: str, + root: str, + verbose: bool, +) -> Dict[str, Any]: """Try to determine the version from the parent directory name. Source tarballs conventionally unpack into a directory that includes both @@ -116,7 +143,7 @@ def versions_from_parentdir(parentdir_prefix, root, verbose): """ rootdirs = [] - for i in range(3): + for _ in range(3): dirname = os.path.basename(root) if dirname.startswith(parentdir_prefix): return { @@ -126,9 +153,8 @@ def versions_from_parentdir(parentdir_prefix, root, verbose): "error": None, "date": None, } - else: - rootdirs.append(root) - root = os.path.dirname(root) # up a level + rootdirs.append(root) + root = os.path.dirname(root) # up a level if verbose: print( @@ -139,41 +165,48 @@ def versions_from_parentdir(parentdir_prefix, root, verbose): @register_vcs_handler("git", "get_keywords") -def git_get_keywords(versionfile_abs): +def git_get_keywords(versionfile_abs: str) -> Dict[str, str]: """Extract version information from the given file.""" # the code embedded in _version.py can just fetch the value of these # keywords. When used from setup.py, we don't want to import _version.py, # so we do it with a regexp instead. This function is not used from # _version.py. - keywords = {} + keywords: Dict[str, str] = {} try: - f = open(versionfile_abs, "r") - for line in f.readlines(): - if line.strip().startswith("git_refnames ="): - mo = re.search(r'=\s*"(.*)"', line) - if mo: - keywords["refnames"] = mo.group(1) - if line.strip().startswith("git_full ="): - mo = re.search(r'=\s*"(.*)"', line) - if mo: - keywords["full"] = mo.group(1) - if line.strip().startswith("git_date ="): - mo = re.search(r'=\s*"(.*)"', line) - if mo: - keywords["date"] = mo.group(1) - f.close() - except EnvironmentError: + with open(versionfile_abs, "r") as fobj: + for line in fobj: + if line.strip().startswith("git_refnames ="): + mo = re.search(r'=\s*"(.*)"', line) + if mo: + keywords["refnames"] = mo.group(1) + if line.strip().startswith("git_full ="): + mo = re.search(r'=\s*"(.*)"', line) + if mo: + keywords["full"] = mo.group(1) + if line.strip().startswith("git_date ="): + mo = re.search(r'=\s*"(.*)"', line) + if mo: + keywords["date"] = mo.group(1) + except OSError: pass return keywords @register_vcs_handler("git", "keywords") -def git_versions_from_keywords(keywords, tag_prefix, verbose): +def git_versions_from_keywords( + keywords: Dict[str, str], + tag_prefix: str, + verbose: bool, +) -> Dict[str, Any]: """Get version information from git keywords.""" - if not keywords: - raise NotThisMethod("no keywords at all, weird") + if "refnames" not in keywords: + raise NotThisMethod("Short version file found") date = keywords.get("date") if date is not None: + # Use only the last line. Previous lines may contain GPG signature + # information. + date = date.splitlines()[-1] + # git-2.2.0 added "%cI", which expands to an ISO-8601 -compliant # datestamp. However we prefer "%ci" (which expands to an "ISO-8601 # -like" string, which we must then edit to make compliant), because @@ -186,11 +219,11 @@ def git_versions_from_keywords(keywords, tag_prefix, verbose): if verbose: print("keywords are unexpanded, not using") raise NotThisMethod("unexpanded keywords, not a git-archive tarball") - refs = set([r.strip() for r in refnames.strip("()").split(",")]) + refs = {r.strip() for r in refnames.strip("()").split(",")} # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of # just "foo-1.0". If we see a "tag: " prefix, prefer those. TAG = "tag: " - tags = set([r[len(TAG) :] for r in refs if r.startswith(TAG)]) + tags = {r[len(TAG) :] for r in refs if r.startswith(TAG)} if not tags: # Either we're using git < 1.8.3, or there really are no tags. We use # a heuristic: assume all version tags have a digit. The old git %d @@ -199,7 +232,7 @@ def git_versions_from_keywords(keywords, tag_prefix, verbose): # between branches and tags. By ignoring refnames without digits, we # filter out many common branch names like "release" and # "stabilization", as well as "HEAD" and "master". - tags = set([r for r in refs if re.search(r"\d", r)]) + tags = {r for r in refs if re.search(r"\d", r)} if verbose: print("discarding '%s', no digits" % ",".join(refs - tags)) if verbose: @@ -208,6 +241,11 @@ def git_versions_from_keywords(keywords, tag_prefix, verbose): # sorting will prefer e.g. "2.0" over "2.0rc1" if ref.startswith(tag_prefix): r = ref[len(tag_prefix) :] + # Filter out refs that exactly match prefix or that don't start + # with a number once the prefix is stripped (mostly a concern + # when prefix is '') + if not re.match(r"\d", r): + continue if verbose: print("picking %s" % r) return { @@ -230,7 +268,9 @@ def git_versions_from_keywords(keywords, tag_prefix, verbose): @register_vcs_handler("git", "pieces_from_vcs") -def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): +def git_pieces_from_vcs( + tag_prefix: str, root: str, verbose: bool, runner: Callable = run_command +) -> Dict[str, Any]: """Get version from 'git describe' in the root of the source tree. This only gets called if the git-archive 'subst' keywords were *not* @@ -241,7 +281,14 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): if sys.platform == "win32": GITS = ["git.cmd", "git.exe"] - out, rc = run_command(GITS, ["rev-parse", "--git-dir"], cwd=root, hide_stderr=True) + # GIT_DIR can interfere with correct operation of Versioneer. + # It may be intended to be passed to the Versioneer-versioned project, + # but that should not change where we get our version from. + env = os.environ.copy() + env.pop("GIT_DIR", None) + runner = functools.partial(runner, env=env) + + _, rc = runner(GITS, ["rev-parse", "--git-dir"], cwd=root, hide_stderr=not verbose) if rc != 0: if verbose: print("Directory %s not under git control" % root) @@ -249,7 +296,7 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): # if there is a tag matching tag_prefix, this yields TAG-NUM-gHEX[-dirty] # if there isn't one, this yields HEX[-dirty] (no NUM) - describe_out, rc = run_command( + describe_out, rc = runner( GITS, [ "describe", @@ -258,7 +305,7 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): "--always", "--long", "--match", - "%s*" % tag_prefix, + f"{tag_prefix}[[:digit:]]*", ], cwd=root, ) @@ -266,16 +313,48 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): if describe_out is None: raise NotThisMethod("'git describe' failed") describe_out = describe_out.strip() - full_out, rc = run_command(GITS, ["rev-parse", "HEAD"], cwd=root) + full_out, rc = runner(GITS, ["rev-parse", "HEAD"], cwd=root) if full_out is None: raise NotThisMethod("'git rev-parse' failed") full_out = full_out.strip() - pieces = {} + pieces: Dict[str, Any] = {} pieces["long"] = full_out pieces["short"] = full_out[:7] # maybe improved later pieces["error"] = None + branch_name, rc = runner(GITS, ["rev-parse", "--abbrev-ref", "HEAD"], cwd=root) + # --abbrev-ref was added in git-1.6.3 + if rc != 0 or branch_name is None: + raise NotThisMethod("'git rev-parse --abbrev-ref' returned error") + branch_name = branch_name.strip() + + if branch_name == "HEAD": + # If we aren't exactly on a branch, pick a branch which represents + # the current commit. If all else fails, we are on a branchless + # commit. + branches, rc = runner(GITS, ["branch", "--contains"], cwd=root) + # --contains was added in git-1.5.4 + if rc != 0 or branches is None: + raise NotThisMethod("'git branch --contains' returned error") + branches = branches.split("\n") + + # Remove the first line if we're running detached + if "(" in branches[0]: + branches.pop(0) + + # Strip off the leading "* " from the list of branches. + branches = [branch[2:] for branch in branches] + if "master" in branches: + branch_name = "master" + elif not branches: + branch_name = None + else: + # Pick the first branch that is returned. Good or bad. + branch_name = branches[0] + + pieces["branch"] = branch_name + # parse describe_out. It will be like TAG-NUM-gHEX[-dirty] or HEX[-dirty] # TAG might have hyphens. git_describe = describe_out @@ -292,7 +371,7 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): # TAG-NUM-gHEX mo = re.search(r"^(.+)-(\d+)-g([0-9a-f]+)$", git_describe) if not mo: - # unparseable. Maybe git-describe is misbehaving? + # unparsable. Maybe git-describe is misbehaving? pieces["error"] = "unable to parse git-describe output: '%s'" % describe_out return pieces @@ -318,26 +397,27 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): else: # HEX: no tags pieces["closest-tag"] = None - count_out, rc = run_command(GITS, ["rev-list", "HEAD", "--count"], cwd=root) - pieces["distance"] = int(count_out) # total number of commits + out, rc = runner(GITS, ["rev-list", "HEAD", "--left-right"], cwd=root) + pieces["distance"] = len(out.split()) # total number of commits # commit date: see ISO-8601 comment in git_versions_from_keywords() - date = run_command(GITS, ["show", "-s", "--format=%ci", "HEAD"], cwd=root)[ - 0 - ].strip() + date = runner(GITS, ["show", "-s", "--format=%ci", "HEAD"], cwd=root)[0].strip() + # Use only the last line. Previous lines may contain GPG signature + # information. + date = date.splitlines()[-1] pieces["date"] = date.strip().replace(" ", "T", 1).replace(" ", "", 1) return pieces -def plus_or_dot(pieces): +def plus_or_dot(pieces: Dict[str, Any]) -> str: """Return a + if we don't already have one, else return a .""" if "+" in pieces.get("closest-tag", ""): return "." return "+" -def render_pep440(pieces): +def render_pep440(pieces: Dict[str, Any]) -> str: """Build up version string, with post-release "local version identifier". Our goal: TAG[+DISTANCE.gHEX[.dirty]] . Note that if you @@ -361,23 +441,70 @@ def render_pep440(pieces): return rendered -def render_pep440_pre(pieces): - """TAG[.post.devDISTANCE] -- No -dirty. +def render_pep440_branch(pieces: Dict[str, Any]) -> str: + """TAG[[.dev0]+DISTANCE.gHEX[.dirty]] . + + The ".dev0" means not master branch. Note that .dev0 sorts backwards + (a feature branch will appear "older" than the master branch). Exceptions: - 1: no tags. 0.post.devDISTANCE + 1: no tags. 0[.dev0]+untagged.DISTANCE.gHEX[.dirty] """ if pieces["closest-tag"]: rendered = pieces["closest-tag"] + if pieces["distance"] or pieces["dirty"]: + if pieces["branch"] != "master": + rendered += ".dev0" + rendered += plus_or_dot(pieces) + rendered += "%d.g%s" % (pieces["distance"], pieces["short"]) + if pieces["dirty"]: + rendered += ".dirty" + else: + # exception #1 + rendered = "0" + if pieces["branch"] != "master": + rendered += ".dev0" + rendered += "+untagged.%d.g%s" % (pieces["distance"], pieces["short"]) + if pieces["dirty"]: + rendered += ".dirty" + return rendered + + +def pep440_split_post(ver: str) -> Tuple[str, Optional[int]]: + """Split pep440 version string at the post-release segment. + + Returns the release segments before the post-release and the + post-release version number (or -1 if no post-release segment is present). + """ + vc = str.split(ver, ".post") + return vc[0], int(vc[1] or 0) if len(vc) == 2 else None + + +def render_pep440_pre(pieces: Dict[str, Any]) -> str: + """TAG[.postN.devDISTANCE] -- No -dirty. + + Exceptions: + 1: no tags. 0.post0.devDISTANCE + """ + if pieces["closest-tag"]: if pieces["distance"]: - rendered += ".post.dev%d" % pieces["distance"] + # update the post release segment + tag_version, post_version = pep440_split_post(pieces["closest-tag"]) + rendered = tag_version + if post_version is not None: + rendered += ".post%d.dev%d" % (post_version + 1, pieces["distance"]) + else: + rendered += ".post0.dev%d" % (pieces["distance"]) + else: + # no commits, use the tag as the version + rendered = pieces["closest-tag"] else: # exception #1 - rendered = "0.post.dev%d" % pieces["distance"] + rendered = "0.post0.dev%d" % pieces["distance"] return rendered -def render_pep440_post(pieces): +def render_pep440_post(pieces: Dict[str, Any]) -> str: """TAG[.postDISTANCE[.dev0]+gHEX] . The ".dev0" means dirty. Note that .dev0 sorts backwards @@ -404,12 +531,41 @@ def render_pep440_post(pieces): return rendered -def render_pep440_old(pieces): +def render_pep440_post_branch(pieces: Dict[str, Any]) -> str: + """TAG[.postDISTANCE[.dev0]+gHEX[.dirty]] . + + The ".dev0" means not master branch. + + Exceptions: + 1: no tags. 0.postDISTANCE[.dev0]+gHEX[.dirty] + """ + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + if pieces["distance"] or pieces["dirty"]: + rendered += ".post%d" % pieces["distance"] + if pieces["branch"] != "master": + rendered += ".dev0" + rendered += plus_or_dot(pieces) + rendered += "g%s" % pieces["short"] + if pieces["dirty"]: + rendered += ".dirty" + else: + # exception #1 + rendered = "0.post%d" % pieces["distance"] + if pieces["branch"] != "master": + rendered += ".dev0" + rendered += "+g%s" % pieces["short"] + if pieces["dirty"]: + rendered += ".dirty" + return rendered + + +def render_pep440_old(pieces: Dict[str, Any]) -> str: """TAG[.postDISTANCE[.dev0]] . The ".dev0" means dirty. - Eexceptions: + Exceptions: 1: no tags. 0.postDISTANCE[.dev0] """ if pieces["closest-tag"]: @@ -426,7 +582,7 @@ def render_pep440_old(pieces): return rendered -def render_git_describe(pieces): +def render_git_describe(pieces: Dict[str, Any]) -> str: """TAG[-DISTANCE-gHEX][-dirty]. Like 'git describe --tags --dirty --always'. @@ -446,7 +602,7 @@ def render_git_describe(pieces): return rendered -def render_git_describe_long(pieces): +def render_git_describe_long(pieces: Dict[str, Any]) -> str: """TAG-DISTANCE-gHEX[-dirty]. Like 'git describe --tags --dirty --always -long'. @@ -466,7 +622,7 @@ def render_git_describe_long(pieces): return rendered -def render(pieces, style): +def render(pieces: Dict[str, Any], style: str) -> Dict[str, Any]: """Render the given version pieces into the requested style.""" if pieces["error"]: return { @@ -482,10 +638,14 @@ def render(pieces, style): if style == "pep440": rendered = render_pep440(pieces) + elif style == "pep440-branch": + rendered = render_pep440_branch(pieces) elif style == "pep440-pre": rendered = render_pep440_pre(pieces) elif style == "pep440-post": rendered = render_pep440_post(pieces) + elif style == "pep440-post-branch": + rendered = render_pep440_post_branch(pieces) elif style == "pep440-old": rendered = render_pep440_old(pieces) elif style == "git-describe": @@ -504,7 +664,7 @@ def render(pieces, style): } -def get_versions(): +def get_versions() -> Dict[str, Any]: """Get version information or return default if unable to do so.""" # I am in _version.py, which lives at ROOT/VERSIONFILE_SOURCE. If we have # __file__, we can work backwards from there to the root. Some @@ -524,7 +684,7 @@ def get_versions(): # versionfile_source is the relative path from the top of the source # tree (where the .git directory might live) to this file. Invert # this to find the root from __file__. - for i in cfg.versionfile_source.split("/"): + for _ in cfg.versionfile_source.split("/"): root = os.path.dirname(root) except NameError: return { diff --git a/lib/jnpr/junos/command/__init__.py b/lib/jnpr/junos/command/__init__.py index 229ab6210..1acaf5b43 100644 --- a/lib/jnpr/junos/command/__init__.py +++ b/lib/jnpr/junos/command/__init__.py @@ -1,32 +1,34 @@ -import sys import os -import yaml -import types - -from jnpr.junos.factory.factory_loader import FactoryLoader +import sys +from importlib.abc import Loader, MetaPathFinder +from importlib.util import spec_from_loader +import yaml import yamlordereddictloader +from jnpr.junos.factory.factory_loader import FactoryLoader __all__ = [] -class MetaPathFinder(object): +class MetaPathFinder(MetaPathFinder): def find_module(self, fullname, path=None): mod = fullname.split(".")[-1] if mod in [ os.path.splitext(i)[0] for i in os.listdir(os.path.dirname(__file__)) ]: - return MetaPathLoader() + return spec_from_loader(fullname, MetaPathLoader(fullname)) -class MetaPathLoader(object): - def load_module(self, fullname): - if fullname in sys.modules: - return sys.modules[fullname] - mod = fullname.split(".")[-1] - modObj = types.ModuleType( - mod, "Module created to provide a context for %s" % mod - ) +class MetaPathLoader(Loader): + def __init__(self, fullname): + self.fullname = fullname + self.modules = {} + + def exec_module(self, module): + if self.fullname in self.modules: + return self.modules[self.fullname] + + mod = self.fullname.split(".")[-1] with open(os.path.join(os.path.dirname(__file__), mod + ".yml"), "r") as stream: try: modules = FactoryLoader().load( @@ -34,10 +36,13 @@ def load_module(self, fullname): ) except yaml.YAMLError as exc: raise ImportError("%s is not loaded" % mod) + for k, v in modules.items(): - setattr(modObj, k, v) - sys.modules[fullname] = modObj - return modObj + setattr(module, k, v) + + self.modules[self.fullname] = module + + return module sys.meta_path.insert(0, MetaPathFinder()) diff --git a/lib/jnpr/junos/device.py b/lib/jnpr/junos/device.py index a4d09a5de..8fec872c3 100644 --- a/lib/jnpr/junos/device.py +++ b/lib/jnpr/junos/device.py @@ -43,7 +43,7 @@ from ncclient.operations.third_party.juniper.rpc import ExecuteRpc import inspect -if sys.version_info.major >= 3: +if sys.version_info[0] >= 3: NCCLIENT_FILTER_XML = len(inspect.signature(ExecuteRpc.request).parameters) == 3 else: NCCLIENT_FILTER_XML = len(inspect.getargspec(ExecuteRpc.request).args) == 3 diff --git a/lib/jnpr/junos/op/__init__.py b/lib/jnpr/junos/op/__init__.py index 825682fca..7fb31a0cb 100644 --- a/lib/jnpr/junos/op/__init__.py +++ b/lib/jnpr/junos/op/__init__.py @@ -1,31 +1,35 @@ import sys import os import yaml -import types +from importlib.abc import Loader, MetaPathFinder +from importlib.util import spec_from_loader from jnpr.junos.factory.factory_loader import FactoryLoader __all__ = [] -class MetaPathFinder(object): - def find_module(self, fullname, path=None): +class OPMetaPathFinder(MetaPathFinder): + def find_spec(self, fullname, path=None, target=None): if fullname.startswith("jnpr.junos"): mod = fullname.split(".")[-1] if mod in [ os.path.splitext(i)[0] for i in os.listdir(os.path.dirname(__file__)) ]: - return MetaPathLoader() + return spec_from_loader(fullname, OPMetaPathLoader(fullname)) -class MetaPathLoader(object): - def load_module(self, fullname): - if fullname in sys.modules: - return sys.modules[fullname] - mod = fullname.split(".")[-1] - modObj = types.ModuleType( - mod, "Module created to provide a context for %s" % mod - ) +class OPMetaPathLoader(Loader): + def __init__(self, fullname): + self.fullname = fullname + self.modules = {} + + def exec_module(self, module): + if self.fullname in self.modules: + return self.modules[self.fullname] + + mod = self.fullname.split(".")[-1] + with open(os.path.join(os.path.dirname(__file__), mod + ".yml"), "r") as stream: try: modules = FactoryLoader().load( @@ -34,9 +38,11 @@ def load_module(self, fullname): except yaml.YAMLError as exc: raise ImportError("%s is not loaded" % mod) for k, v in modules.items(): - setattr(modObj, k, v) - sys.modules[fullname] = modObj - return modObj + setattr(module, k, v) + + self.modules[self.fullname] = module + + return module -sys.meta_path.insert(0, MetaPathFinder()) +sys.meta_path.insert(0, OPMetaPathFinder()) diff --git a/lib/jnpr/junos/rpcmeta.py b/lib/jnpr/junos/rpcmeta.py index e59ea3a58..e1bc8f118 100644 --- a/lib/jnpr/junos/rpcmeta.py +++ b/lib/jnpr/junos/rpcmeta.py @@ -90,7 +90,7 @@ def get_config( options={'database':'committed','inherit':'inherit'}) :param str model: Can provide yang model openconfig/custom/ietf. When - model is True (filter_xml option is not supported), xml is enclosed under + model is True and filter_xml is None, xml is enclosed under so that we get junos as well as other model configurations diff --git a/lib/jnpr/junos/utils/scp.py b/lib/jnpr/junos/utils/scp.py index 6bfc727b3..36f179cab 100644 --- a/lib/jnpr/junos/utils/scp.py +++ b/lib/jnpr/junos/utils/scp.py @@ -43,7 +43,7 @@ def __init__(self, junos, **scpargs): # User case also define progress with 3 params, the way scp module # expects. Function will take path, total size, transferred. # https://github.com/jbardin/scp.py/blob/master/scp.py#L97 - spec = inspect.getargspec(self._user_progress) + spec = inspect.getfullargspec(self._user_progress) if (len(spec.args) == 3 and spec.args[0] != "self") or ( len(spec.args) == 4 and spec.args[0] == "self" ): diff --git a/lib/jnpr/junos/utils/start_shell.py b/lib/jnpr/junos/utils/start_shell.py index f78e93e06..cef26eddc 100644 --- a/lib/jnpr/junos/utils/start_shell.py +++ b/lib/jnpr/junos/utils/start_shell.py @@ -84,7 +84,8 @@ def wait_for(self, this=_SHELL_PROMPT, timeout=0, sleep=0): if isinstance(data, bytes): data = data.decode("utf-8", "replace") got.append(data) - if this is not None and re.search(r"{}\s?$".format(this), data): + + if this is not None and re.search(r"{}\s?$".format(this), str(data)): break return got @@ -119,7 +120,7 @@ def open(self): stdin=subprocess.PIPE, stdout=subprocess.PIPE, close_fds=1, - bufsize=1, + bufsize=0, ) else: self._client = open_ssh_client(dev=self._nc) diff --git a/requirements.txt b/requirements.txt index 798c7b32f..c18bc21d0 100755 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ lxml>=3.2.4 # ncclient version 0.6.10 has issues with PyEZ(junos-eznc) and needs to be avoided -ncclient==0.6.13 +ncclient>=0.6.15 paramiko>=1.15.2 scp>=0.7.0 jinja2>=2.7.1 diff --git a/setup.py b/setup.py index de0dd9d21..2b72f03f8 100755 --- a/setup.py +++ b/setup.py @@ -24,7 +24,7 @@ "jnpr.junos.cfgro": ["*.yml"], "jnpr.junos.resources": ["*.yml"], }, - python_requires=">=3.5, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*", + python_requires=">=3.8", install_requires=install_reqs, classifiers=[ "Development Status :: 5 - Production/Stable", @@ -36,10 +36,9 @@ "License :: OSI Approved :: Apache Software License", "Operating System :: OS Independent", "Programming Language :: Python", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.9", "Topic :: Software Development :: Libraries", "Topic :: Software Development :: Libraries :: Application Frameworks", "Topic :: Software Development :: Libraries :: Python Modules", diff --git a/tests/functional/test_core.py b/tests/functional/test_core.py index 5748f912e..c99c73b1c 100644 --- a/tests/functional/test_core.py +++ b/tests/functional/test_core.py @@ -4,11 +4,10 @@ import unittest2 as unittest except ImportError: import unittest -from nose.plugins.attrib import attr + from jnpr.junos.exception import RpcTimeoutError -@attr("functional") class TestCore(unittest.TestCase): @classmethod def setUpClass(self): diff --git a/tests/functional/test_device_ssh.py b/tests/functional/test_device_ssh.py index a730cd81e..1302a05ce 100644 --- a/tests/functional/test_device_ssh.py +++ b/tests/functional/test_device_ssh.py @@ -1,12 +1,10 @@ __author__ = "rsherman, vnitinv" import unittest -from nose.plugins.attrib import attr from jnpr.junos import Device -@attr("functional") class TestDeviceSsh(unittest.TestCase): def tearDown(self): self.dev.close() diff --git a/tests/functional/test_outbound_ssh.py b/tests/functional/test_outbound_ssh.py index 7f7804d01..db5126fa8 100644 --- a/tests/functional/test_outbound_ssh.py +++ b/tests/functional/test_outbound_ssh.py @@ -1,13 +1,11 @@ __author__ = "mwiget" import unittest -from nose.plugins.attrib import attr import socket from jnpr.junos import Device -@attr("functional") class TestDeviceSsh(unittest.TestCase): def tearDown(self): self.dev.close() diff --git a/tests/functional/test_shell.py b/tests/functional/test_shell.py index 7e55e59c5..df62e9061 100644 --- a/tests/functional/test_shell.py +++ b/tests/functional/test_shell.py @@ -5,16 +5,14 @@ except ImportError: import unittest -from nose.plugins.attrib import attr import yaml -@attr('functional') class test(unittest.TestCase): - @classmethod def setUpClass(self): from jnpr.junos import Device + with open("config.yaml", "r") as fyaml: cfg = yaml.safe_load(fyaml) self.dev = Device(**cfg) @@ -26,24 +24,28 @@ def tearDownClass(self): def test_shell_run(self): from jnpr.junos.utils.start_shell import StartShell + with StartShell(self.dev) as sh: - output = sh.run('pwd') + output = sh.run("pwd") self.assertTrue(output[0]) def test_shell_run_with_sleep(self): from jnpr.junos.utils.start_shell import StartShell + with StartShell(self.dev) as sh: - output = sh.run('hostname', sleep=2) + output = sh.run("hostname", sleep=2) self.assertTrue(output[0]) def test_shell_run_shell_type_ssh(self): from jnpr.junos.utils.start_shell import StartShell + with StartShell(self.dev, shell_type="ssh") as sh: - output = sh.run('hostname', sleep=2) + output = sh.run("hostname", sleep=2) self.assertTrue(output[0]) def test_shell_run_shell_type_csh(self): from jnpr.junos.utils.start_shell import StartShell + with StartShell(self.dev, shell_type="csh") as sh: - output = sh.run('hostname', sleep=2) + output = sh.run("hostname", sleep=2) self.assertTrue(output[0]) diff --git a/tests/functional/test_table.py b/tests/functional/test_table.py index a60a6ca03..0f3ff258d 100644 --- a/tests/functional/test_table.py +++ b/tests/functional/test_table.py @@ -4,13 +4,11 @@ import unittest2 as unittest except ImportError: import unittest -from nose.plugins.attrib import attr from jnpr.junos.op.routes import RouteTable import json -@attr("functional") class TestTable(unittest.TestCase): @classmethod def setUpClass(self): diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py index 07bc7e0da..dd3388c69 100644 --- a/tests/unit/__init__.py +++ b/tests/unit/__init__.py @@ -1,17 +1,16 @@ import unittest import sys -from nose.plugins.attrib import attr +import nose2 from mock import patch __author__ = "Nitin Kumar" __credits__ = "Jeremy Schulman" -@attr("unit") class TestJunosInit(unittest.TestCase): def test_warning(self): - with patch.object(sys.modules["sys"], "version_info", (2, 6, 3)) as mock_sys: + with patch.object(sys.modules["sys"], "version_info", (3, 8, 0)) as mock_sys: from jnpr import junos - self.assertEqual(mock_sys, (2, 6, 3)) + self.assertEqual(mock_sys, (3, 8, 0)) diff --git a/tests/unit/factory/test_cfgtable.py b/tests/unit/factory/test_cfgtable.py index f9e6eadd3..0ff618fde 100644 --- a/tests/unit/factory/test_cfgtable.py +++ b/tests/unit/factory/test_cfgtable.py @@ -5,7 +5,7 @@ import os import sys -from nose.plugins.attrib import attr +import nose2 import yaml from jnpr.junos import Device @@ -85,7 +85,6 @@ globals().update(FactoryLoader().load(yaml.load(yaml_bgp_data, Loader=yaml.FullLoader))) -@attr("unit") @unittest.skipIf(sys.platform == "win32", "will work for windows in coming days") class TestFactoryCfgTable(unittest.TestCase): @patch("ncclient.manager.connect") diff --git a/tests/unit/factory/test_cmdtable.py b/tests/unit/factory/test_cmdtable.py index d74e02352..177c6e6a0 100644 --- a/tests/unit/factory/test_cmdtable.py +++ b/tests/unit/factory/test_cmdtable.py @@ -3,7 +3,7 @@ import unittest import os -from nose.plugins.attrib import attr +import nose2 from jnpr.junos import Device from jnpr.junos.exception import RpcError @@ -17,7 +17,6 @@ import json -@attr("unit") class TestFactoryCMDTable(unittest.TestCase): @patch("ncclient.manager.connect") def setUp(self, mock_connect): @@ -2169,132 +2168,132 @@ def test_table_eval_with_filters(self, mock_execute): self.assertEqual( dict(stats), { - u"100ms Periodic": {u"cpu": u"0", u"state": u"asleep"}, - u"10s Low Periodic": {u"cpu": u"0", u"state": u"asleep"}, - u"10s Medium Periodic": {u"cpu": u"0", u"state": u"asleep"}, - u"1s Low Periodic": {u"cpu": u"0", u"state": u"asleep"}, - u"1s Medium Periodic": {u"cpu": u"0", u"state": u"asleep"}, - u"50ms Periodic": {u"cpu": u"0", u"state": u"asleep"}, - u"CFM Data thread": {u"cpu": u"0", u"state": u"asleep"}, - u"CFM Manager": {u"cpu": u"0", u"state": u"asleep"}, - u"CFP": {u"cpu": u"0", u"state": u"asleep"}, - u"CLKSYNC Manager": {u"cpu": u"0", u"state": u"asleep"}, - u"CLNS Err Input": {u"cpu": u"0", u"state": u"asleep"}, - u"CLNS Option Input": {u"cpu": u"0", u"state": u"asleep"}, - u"CXP": {u"cpu": u"0", u"state": u"asleep"}, - u"Cassis Free Timer": {u"cpu": u"3", u"state": u"asleep"}, - u"Cattle-Prod Daemon": {u"cpu": u"0", u"state": u"asleep"}, - u"Console": {u"cpu": u"0", u"state": u"asleep"}, - u"Cube Server": {u"cpu": u"0", u"state": u"asleep"}, - u"DCC Background": {u"cpu": u"0", u"state": u"asleep"}, - u"DDOS Policers": {u"cpu": u"0", u"state": u"asleep"}, - u"DFW Alert": {u"cpu": u"0", u"state": u"asleep"}, - u"DSX50ms": {u"cpu": u"0", u"state": u"asleep"}, - u"DSXonesec": {u"cpu": u"0", u"state": u"asleep"}, - u"Firmware Upgrade": {u"cpu": u"0", u"state": u"asleep"}, - u"GR253": {u"cpu": u"0", u"state": u"asleep"}, - u"HSL2": {u"cpu": u"0", u"state": u"asleep"}, - u"Heap Accouting": {u"cpu": u"0", u"state": u"asleep"}, - u"Host Loopback Periodic": {u"cpu": u"0", u"state": u"asleep"}, - u"ICMP Input": {u"cpu": u"0", u"state": u"asleep"}, - u"ICMP6 Input": {u"cpu": u"0", u"state": u"asleep"}, - u"IFCM": {u"cpu": u"0", u"state": u"asleep"}, - u"IGMP": {u"cpu": u"0", u"state": u"asleep"}, - u"IGMP Input": {u"cpu": u"0", u"state": u"asleep"}, - u"IP Option Input": {u"cpu": u"0", u"state": u"asleep"}, - u"IP Reassembly": {u"cpu": u"0", u"state": u"asleep"}, - u"IP6 Option Input": {u"cpu": u"0", u"state": u"asleep"}, - u"IPC Test Daemon": {u"cpu": u"0", u"state": u"asleep"}, - u"IPv4 PFE Control Background": {u"cpu": u"0", u"state": u"asleep"}, - u"JNH Exception Counter Background Thread": { - u"cpu": u"0", - u"state": u"asleep", - }, - u"JNH KA Transmit": {u"cpu": u"0", u"state": u"asleep"}, - u"JNH Partition Mem Recovery": {u"cpu": u"0", u"state": u"asleep"}, - u"L2ALM Manager": {u"cpu": u"0", u"state": u"asleep"}, - u"L2PD": {u"cpu": u"0", u"state": u"asleep"}, - u"L2TP-SF KA Transmit": {u"cpu": u"0", u"state": u"asleep"}, - u"LKUP ASIC UCODE Rebalance Service": { - u"cpu": u"1", - u"state": u"asleep", - }, - u"LKUP ASIC Wedge poll thread": {u"cpu": u"0", u"state": u"asleep"}, - u"LU Background Service": {u"cpu": u"4", u"state": u"asleep"}, - u"LU-CNTR Reader": {u"cpu": u"0", u"state": u"asleep"}, - u"MSA300PIN": {u"cpu": u"0", u"state": u"asleep"}, - u"Maintenance": {u"cpu": u"0", u"state": u"asleep"}, - u"NH Probe Service": {u"cpu": u"0", u"state": u"asleep"}, - u"OTN": {u"cpu": u"0", u"state": u"asleep"}, - u"PFE Manager": {u"cpu": u"0", u"state": u"asleep"}, - u"PFE Statistics": {u"cpu": u"0", u"state": u"asleep"}, - u"PFEMAN SRRD Thread": {u"cpu": u"0", u"state": u"asleep"}, - u"PFEMAN Service Thread": {u"cpu": u"0", u"state": u"asleep"}, - u"PIC": {u"cpu": u"0", u"state": u"asleep"}, - u"PIC Periodic": {u"cpu": u"0", u"state": u"asleep"}, - u"PPM Data thread": {u"cpu": u"0", u"state": u"asleep"}, - u"PPM Manager": {u"cpu": u"0", u"state": u"asleep"}, - u"PQ3 PCI Periodic": {u"cpu": u"0", u"state": u"asleep"}, - u"PRECL Chip Generic": {u"cpu": u"0", u"state": u"asleep"}, - u"PZARB Timeout": {u"cpu": u"0", u"state": u"asleep"}, - u"Pfesvcsor": {u"cpu": u"0", u"state": u"asleep"}, - u"QSFP": {u"cpu": u"0", u"state": u"asleep"}, - u"RCM Pfe Manager": {u"cpu": u"0", u"state": u"asleep"}, - u"RDMAN": {u"cpu": u"0", u"state": u"asleep"}, - u"RDP Input": {u"cpu": u"0", u"state": u"asleep"}, - u"RDP Timers": {u"cpu": u"0", u"state": u"asleep"}, - u"RFC2544 periodic": {u"cpu": u"0", u"state": u"asleep"}, - u"RPM Msg thread": {u"cpu": u"0", u"state": u"asleep"}, - u"RSMON syslog thread": {u"cpu": u"0", u"state": u"asleep"}, - u"SFP": {u"cpu": u"0", u"state": u"asleep"}, - u"SNTP Daemon": {u"cpu": u"0", u"state": u"asleep"}, - u"Services TOD": {u"cpu": u"0", u"state": u"asleep"}, - u"Sheaf Background": {u"cpu": u"0", u"state": u"asleep"}, - u"Stats Page Ager": {u"cpu": u"0", u"state": u"asleep"}, - u"Syslog": {u"cpu": u"0", u"state": u"asleep"}, - u"TCP Receive": {u"cpu": u"0", u"state": u"asleep"}, - u"TCP Timers": {u"cpu": u"0", u"state": u"asleep"}, - u"TNP Hello": {u"cpu": u"0", u"state": u"asleep"}, - u"TNPC CM": {u"cpu": u"0", u"state": u"asleep"}, - u"TOE Coredump": {u"cpu": u"0", u"state": u"asleep"}, - u"TTP Receive": {u"cpu": u"0", u"state": u"asleep"}, - u"TTP Transmit": {u"cpu": u"0", u"state": u"asleep"}, - u"TTRACE Creator": {u"cpu": u"0", u"state": u"asleep"}, - u"TTRACE Tracer": {u"cpu": u"0", u"state": u"asleep"}, - u"Timer Services": {u"cpu": u"0", u"state": u"asleep"}, - u"Trap_Info Read PFE 0.0": {u"cpu": u"0", u"state": u"asleep"}, - u"Trap_Info Read PFE 0.1": {u"cpu": u"0", u"state": u"asleep"}, - u"Trap_Info Read PFE 1.0": {u"cpu": u"0", u"state": u"asleep"}, - u"Trap_Info Read PFE 1.1": {u"cpu": u"0", u"state": u"asleep"}, - u"UDP Input": {u"cpu": u"0", u"state": u"asleep"}, - u"Ukern Syslog": {u"cpu": u"0", u"state": u"asleep"}, - u"VBF MC Purge": {u"cpu": u"0", u"state": u"asleep"}, - u"VBF PFE Events": {u"cpu": u"0", u"state": u"asleep"}, - u"VBF Walker": {u"cpu": u"0", u"state": u"asleep"}, - u"VRRP Manager": {u"cpu": u"0", u"state": u"asleep"}, - u"Virtual Console": {u"cpu": u"0", u"state": u"asleep"}, - u"XFP": {u"cpu": u"0", u"state": u"asleep"}, - u"XM Chip Generic": {u"cpu": u"0", u"state": u"asleep"}, - u"XM Chip Statistics": {u"cpu": u"0", u"state": u"asleep"}, - u"XM Chip Wedge Detection and Recovery": { - u"cpu": u"0", - u"state": u"asleep", - }, - u"bulkget Manager": {u"cpu": u"0", u"state": u"asleep"}, - u"cos halp stats daemon": {u"cpu": u"0", u"state": u"asleep"}, - u"jnh errors daemon": {u"cpu": u"0", u"state": u"asleep"}, - u"mac_db": {u"cpu": u"0", u"state": u"asleep"}, - u"zlAprTaskzl303xx": {u"cpu": u"0", u"state": u"asleep"}, - u"zlHybridTaskzl303xx": {u"cpu": u"0", u"state": u"asleep"}, - u"zlInt08Taskzl303xx": {u"cpu": u"1", u"state": u"asleep"}, - u"zlInt09Taskzl303xx": {u"cpu": u"0", u"state": u"asleep"}, - u"zlPktTxSchedTaskzl303xx": {u"cpu": u"0", u"state": u"asleep"}, - u"zlPtpTaskzl303xx": {u"cpu": u"0", u"state": u"asleep"}, - u"zlSpllTaskzl303xx": {u"cpu": u"0", u"state": u"asleep"}, - u"zlTimerTaskzl303xx": {u"cpu": u"0", u"state": u"asleep"}, - u"zlTodMgrTaskzl303xx": {u"cpu": u"0", u"state": u"asleep"}, - u"zlTsEngTaskzl303xx": {u"cpu": u"0", u"state": u"asleep"}, - u"zlTxTsMgrTaskzl303xx": {u"cpu": u"0", u"state": u"asleep"}, + "100ms Periodic": {"cpu": "0", "state": "asleep"}, + "10s Low Periodic": {"cpu": "0", "state": "asleep"}, + "10s Medium Periodic": {"cpu": "0", "state": "asleep"}, + "1s Low Periodic": {"cpu": "0", "state": "asleep"}, + "1s Medium Periodic": {"cpu": "0", "state": "asleep"}, + "50ms Periodic": {"cpu": "0", "state": "asleep"}, + "CFM Data thread": {"cpu": "0", "state": "asleep"}, + "CFM Manager": {"cpu": "0", "state": "asleep"}, + "CFP": {"cpu": "0", "state": "asleep"}, + "CLKSYNC Manager": {"cpu": "0", "state": "asleep"}, + "CLNS Err Input": {"cpu": "0", "state": "asleep"}, + "CLNS Option Input": {"cpu": "0", "state": "asleep"}, + "CXP": {"cpu": "0", "state": "asleep"}, + "Cassis Free Timer": {"cpu": "3", "state": "asleep"}, + "Cattle-Prod Daemon": {"cpu": "0", "state": "asleep"}, + "Console": {"cpu": "0", "state": "asleep"}, + "Cube Server": {"cpu": "0", "state": "asleep"}, + "DCC Background": {"cpu": "0", "state": "asleep"}, + "DDOS Policers": {"cpu": "0", "state": "asleep"}, + "DFW Alert": {"cpu": "0", "state": "asleep"}, + "DSX50ms": {"cpu": "0", "state": "asleep"}, + "DSXonesec": {"cpu": "0", "state": "asleep"}, + "Firmware Upgrade": {"cpu": "0", "state": "asleep"}, + "GR253": {"cpu": "0", "state": "asleep"}, + "HSL2": {"cpu": "0", "state": "asleep"}, + "Heap Accouting": {"cpu": "0", "state": "asleep"}, + "Host Loopback Periodic": {"cpu": "0", "state": "asleep"}, + "ICMP Input": {"cpu": "0", "state": "asleep"}, + "ICMP6 Input": {"cpu": "0", "state": "asleep"}, + "IFCM": {"cpu": "0", "state": "asleep"}, + "IGMP": {"cpu": "0", "state": "asleep"}, + "IGMP Input": {"cpu": "0", "state": "asleep"}, + "IP Option Input": {"cpu": "0", "state": "asleep"}, + "IP Reassembly": {"cpu": "0", "state": "asleep"}, + "IP6 Option Input": {"cpu": "0", "state": "asleep"}, + "IPC Test Daemon": {"cpu": "0", "state": "asleep"}, + "IPv4 PFE Control Background": {"cpu": "0", "state": "asleep"}, + "JNH Exception Counter Background Thread": { + "cpu": "0", + "state": "asleep", + }, + "JNH KA Transmit": {"cpu": "0", "state": "asleep"}, + "JNH Partition Mem Recovery": {"cpu": "0", "state": "asleep"}, + "L2ALM Manager": {"cpu": "0", "state": "asleep"}, + "L2PD": {"cpu": "0", "state": "asleep"}, + "L2TP-SF KA Transmit": {"cpu": "0", "state": "asleep"}, + "LKUP ASIC UCODE Rebalance Service": { + "cpu": "1", + "state": "asleep", + }, + "LKUP ASIC Wedge poll thread": {"cpu": "0", "state": "asleep"}, + "LU Background Service": {"cpu": "4", "state": "asleep"}, + "LU-CNTR Reader": {"cpu": "0", "state": "asleep"}, + "MSA300PIN": {"cpu": "0", "state": "asleep"}, + "Maintenance": {"cpu": "0", "state": "asleep"}, + "NH Probe Service": {"cpu": "0", "state": "asleep"}, + "OTN": {"cpu": "0", "state": "asleep"}, + "PFE Manager": {"cpu": "0", "state": "asleep"}, + "PFE Statistics": {"cpu": "0", "state": "asleep"}, + "PFEMAN SRRD Thread": {"cpu": "0", "state": "asleep"}, + "PFEMAN Service Thread": {"cpu": "0", "state": "asleep"}, + "PIC": {"cpu": "0", "state": "asleep"}, + "PIC Periodic": {"cpu": "0", "state": "asleep"}, + "PPM Data thread": {"cpu": "0", "state": "asleep"}, + "PPM Manager": {"cpu": "0", "state": "asleep"}, + "PQ3 PCI Periodic": {"cpu": "0", "state": "asleep"}, + "PRECL Chip Generic": {"cpu": "0", "state": "asleep"}, + "PZARB Timeout": {"cpu": "0", "state": "asleep"}, + "Pfesvcsor": {"cpu": "0", "state": "asleep"}, + "QSFP": {"cpu": "0", "state": "asleep"}, + "RCM Pfe Manager": {"cpu": "0", "state": "asleep"}, + "RDMAN": {"cpu": "0", "state": "asleep"}, + "RDP Input": {"cpu": "0", "state": "asleep"}, + "RDP Timers": {"cpu": "0", "state": "asleep"}, + "RFC2544 periodic": {"cpu": "0", "state": "asleep"}, + "RPM Msg thread": {"cpu": "0", "state": "asleep"}, + "RSMON syslog thread": {"cpu": "0", "state": "asleep"}, + "SFP": {"cpu": "0", "state": "asleep"}, + "SNTP Daemon": {"cpu": "0", "state": "asleep"}, + "Services TOD": {"cpu": "0", "state": "asleep"}, + "Sheaf Background": {"cpu": "0", "state": "asleep"}, + "Stats Page Ager": {"cpu": "0", "state": "asleep"}, + "Syslog": {"cpu": "0", "state": "asleep"}, + "TCP Receive": {"cpu": "0", "state": "asleep"}, + "TCP Timers": {"cpu": "0", "state": "asleep"}, + "TNP Hello": {"cpu": "0", "state": "asleep"}, + "TNPC CM": {"cpu": "0", "state": "asleep"}, + "TOE Coredump": {"cpu": "0", "state": "asleep"}, + "TTP Receive": {"cpu": "0", "state": "asleep"}, + "TTP Transmit": {"cpu": "0", "state": "asleep"}, + "TTRACE Creator": {"cpu": "0", "state": "asleep"}, + "TTRACE Tracer": {"cpu": "0", "state": "asleep"}, + "Timer Services": {"cpu": "0", "state": "asleep"}, + "Trap_Info Read PFE 0.0": {"cpu": "0", "state": "asleep"}, + "Trap_Info Read PFE 0.1": {"cpu": "0", "state": "asleep"}, + "Trap_Info Read PFE 1.0": {"cpu": "0", "state": "asleep"}, + "Trap_Info Read PFE 1.1": {"cpu": "0", "state": "asleep"}, + "UDP Input": {"cpu": "0", "state": "asleep"}, + "Ukern Syslog": {"cpu": "0", "state": "asleep"}, + "VBF MC Purge": {"cpu": "0", "state": "asleep"}, + "VBF PFE Events": {"cpu": "0", "state": "asleep"}, + "VBF Walker": {"cpu": "0", "state": "asleep"}, + "VRRP Manager": {"cpu": "0", "state": "asleep"}, + "Virtual Console": {"cpu": "0", "state": "asleep"}, + "XFP": {"cpu": "0", "state": "asleep"}, + "XM Chip Generic": {"cpu": "0", "state": "asleep"}, + "XM Chip Statistics": {"cpu": "0", "state": "asleep"}, + "XM Chip Wedge Detection and Recovery": { + "cpu": "0", + "state": "asleep", + }, + "bulkget Manager": {"cpu": "0", "state": "asleep"}, + "cos halp stats daemon": {"cpu": "0", "state": "asleep"}, + "jnh errors daemon": {"cpu": "0", "state": "asleep"}, + "mac_db": {"cpu": "0", "state": "asleep"}, + "zlAprTaskzl303xx": {"cpu": "0", "state": "asleep"}, + "zlHybridTaskzl303xx": {"cpu": "0", "state": "asleep"}, + "zlInt08Taskzl303xx": {"cpu": "1", "state": "asleep"}, + "zlInt09Taskzl303xx": {"cpu": "0", "state": "asleep"}, + "zlPktTxSchedTaskzl303xx": {"cpu": "0", "state": "asleep"}, + "zlPtpTaskzl303xx": {"cpu": "0", "state": "asleep"}, + "zlSpllTaskzl303xx": {"cpu": "0", "state": "asleep"}, + "zlTimerTaskzl303xx": {"cpu": "0", "state": "asleep"}, + "zlTodMgrTaskzl303xx": {"cpu": "0", "state": "asleep"}, + "zlTsEngTaskzl303xx": {"cpu": "0", "state": "asleep"}, + "zlTxTsMgrTaskzl303xx": {"cpu": "0", "state": "asleep"}, }, ) diff --git a/tests/unit/factory/test_factory_cls.py b/tests/unit/factory/test_factory_cls.py index a86a0bb44..276c65c0b 100644 --- a/tests/unit/factory/test_factory_cls.py +++ b/tests/unit/factory/test_factory_cls.py @@ -2,13 +2,12 @@ __credits__ = "Jeremy Schulman" import unittest -from nose.plugins.attrib import attr +import nose2 from jnpr.junos.factory.factory_cls import FactoryCfgTable, FactoryOpTable from jnpr.junos.factory.factory_cls import FactoryTable, FactoryView -@attr("unit") class TestFactoryCls(unittest.TestCase): def test_factory_cls_cfgtable(self): t = FactoryCfgTable() diff --git a/tests/unit/factory/test_factory_loader.py b/tests/unit/factory/test_factory_loader.py index d126e10e9..4a9ac1624 100644 --- a/tests/unit/factory/test_factory_loader.py +++ b/tests/unit/factory/test_factory_loader.py @@ -2,12 +2,11 @@ __credits__ = "Jeremy Schulman" import unittest -from nose.plugins.attrib import attr +import nose2 from jnpr.junos.factory import FactoryLoader from mock import patch -@attr("unit") class TestFactoryLoader(unittest.TestCase): def setUp(self): self.fl = FactoryLoader() diff --git a/tests/unit/factory/test_optable.py b/tests/unit/factory/test_optable.py index 996541221..0c516a7d7 100644 --- a/tests/unit/factory/test_optable.py +++ b/tests/unit/factory/test_optable.py @@ -5,7 +5,7 @@ import os import yaml import json -from nose.plugins.attrib import attr +import nose2 from jnpr.junos import Device from jnpr.junos.op.phyport import PhyPortStatsTable @@ -22,7 +22,6 @@ from mock import patch -@attr("unit") class TestFactoryOpTable(unittest.TestCase): @patch("ncclient.manager.connect") def setUp(self, mock_connect): diff --git a/tests/unit/factory/test_table.py b/tests/unit/factory/test_table.py index 870a723ed..3bcb5c1a7 100644 --- a/tests/unit/factory/test_table.py +++ b/tests/unit/factory/test_table.py @@ -2,7 +2,7 @@ __credits__ = "Jeremy Schulman" import unittest -from nose.plugins.attrib import attr +import nose2 import os from jnpr.junos import Device @@ -23,7 +23,6 @@ builtin_string = "builtins" -@attr("unit") class TestFactoryTable(unittest.TestCase): @patch("ncclient.manager.connect") def setUp(self, mock_connect): diff --git a/tests/unit/factory/test_to_json.py b/tests/unit/factory/test_to_json.py index b6feb2cac..8d9204c05 100644 --- a/tests/unit/factory/test_to_json.py +++ b/tests/unit/factory/test_to_json.py @@ -4,7 +4,7 @@ import unittest2 as unittest except ImportError: import unittest -from nose.plugins.attrib import attr +import nose2 from mock import patch import os import json @@ -21,7 +21,6 @@ from ncclient.operations.rpc import RPCReply -@attr("unit") class TestToJson(unittest.TestCase): @patch("ncclient.manager.connect") def setUp(self, mock_connect): diff --git a/tests/unit/factory/test_view.py b/tests/unit/factory/test_view.py index 2548712a1..455e3171a 100644 --- a/tests/unit/factory/test_view.py +++ b/tests/unit/factory/test_view.py @@ -2,7 +2,7 @@ __credits__ = "Jeremy Schulman" import unittest -from nose.plugins.attrib import attr +import nose2 from mock import MagicMock, patch from jnpr.junos import Device from jnpr.junos.factory.view import View @@ -10,7 +10,6 @@ from lxml import etree -@attr("unit") class TestFactoryView(unittest.TestCase): def setUp(self): self.dev = Device( diff --git a/tests/unit/factory/test_view_fields.py b/tests/unit/factory/test_view_fields.py index 59ad9d2cf..27ceb07cb 100644 --- a/tests/unit/factory/test_view_fields.py +++ b/tests/unit/factory/test_view_fields.py @@ -2,12 +2,11 @@ __credits__ = "Jeremy Schulman" import unittest -from nose.plugins.attrib import attr +import nose2 from jnpr.junos.factory.viewfields import ViewFields -@attr("unit") class TestFactoryViewFields(unittest.TestCase): def setUp(self): self.vf = ViewFields() diff --git a/tests/unit/facts/test__init__.py b/tests/unit/facts/test__init__.py index 5327e63bb..c8348e241 100644 --- a/tests/unit/facts/test__init__.py +++ b/tests/unit/facts/test__init__.py @@ -5,14 +5,13 @@ import unittest2 as unittest except ImportError: import unittest -from nose.plugins.attrib import attr +import nose2 import importlib import sys import jnpr.junos.facts -@attr("unit") class TestFactInitialization(unittest.TestCase): def test_duplicate_facts(self): module = importlib.import_module("tests.unit.facts.dupe_foo1") diff --git a/tests/unit/facts/test_current_re.py b/tests/unit/facts/test_current_re.py index a36f1d7a5..3cc06eac9 100644 --- a/tests/unit/facts/test_current_re.py +++ b/tests/unit/facts/test_current_re.py @@ -2,7 +2,7 @@ __credits__ = "Jeremy Schulman, Nitin Kumar" import unittest -from nose.plugins.attrib import attr +import nose2 from mock import patch, MagicMock import os from lxml import etree @@ -14,7 +14,6 @@ from ncclient.transport import SSHSession -@attr("unit") class TestCurrentRe(unittest.TestCase): @patch("ncclient.manager.connect") def setUp(self, mock_connect): diff --git a/tests/unit/facts/test_domain.py b/tests/unit/facts/test_domain.py index e75b8e2dc..4e46e1438 100644 --- a/tests/unit/facts/test_domain.py +++ b/tests/unit/facts/test_domain.py @@ -2,7 +2,7 @@ __credits__ = "Jeremy Schulman, Nitin Kumar" import unittest -from nose.plugins.attrib import attr +import nose2 from mock import patch, MagicMock import os from lxml import etree @@ -14,7 +14,6 @@ from ncclient.transport import SSHSession -@attr("unit") class TestDomain(unittest.TestCase): @patch("ncclient.manager.connect") def setUp(self, mock_connect): diff --git a/tests/unit/facts/test_ethernet_mac_table.py b/tests/unit/facts/test_ethernet_mac_table.py index f5d1501b3..5e7be1e76 100644 --- a/tests/unit/facts/test_ethernet_mac_table.py +++ b/tests/unit/facts/test_ethernet_mac_table.py @@ -2,7 +2,7 @@ __credits__ = "Jeremy Schulman, Nitin Kumar" import unittest -from nose.plugins.attrib import attr +import nose2 from mock import patch, MagicMock import os from lxml import etree @@ -14,7 +14,6 @@ from ncclient.transport import SSHSession -@attr("unit") class TestEthernetMacTable(unittest.TestCase): @patch("ncclient.manager.connect") def setUp(self, mock_connect): diff --git a/tests/unit/facts/test_file_list.py b/tests/unit/facts/test_file_list.py index 860132f62..7ed58f5cf 100644 --- a/tests/unit/facts/test_file_list.py +++ b/tests/unit/facts/test_file_list.py @@ -2,7 +2,7 @@ __credits__ = "Jeremy Schulman, Nitin Kumar" import unittest -from nose.plugins.attrib import attr +import nose2 from mock import patch, MagicMock import os @@ -12,7 +12,6 @@ from ncclient.transport import SSHSession -@attr("unit") class TestFileList(unittest.TestCase): @patch("ncclient.manager.connect") def setUp(self, mock_connect): diff --git a/tests/unit/facts/test_get_chassis_cluster_status.py b/tests/unit/facts/test_get_chassis_cluster_status.py index 993e81809..932c239c6 100644 --- a/tests/unit/facts/test_get_chassis_cluster_status.py +++ b/tests/unit/facts/test_get_chassis_cluster_status.py @@ -2,7 +2,7 @@ __credits__ = "Jeremy Schulman, Nitin Kumar" import unittest -from nose.plugins.attrib import attr +import nose2 from mock import patch, MagicMock import os from lxml import etree @@ -14,7 +14,6 @@ from ncclient.transport import SSHSession -@attr("unit") class TestGetChassisClusterStatus(unittest.TestCase): @patch("ncclient.manager.connect") def setUp(self, mock_connect): diff --git a/tests/unit/facts/test_get_chassis_inventory.py b/tests/unit/facts/test_get_chassis_inventory.py index 707206f92..3c3dba240 100644 --- a/tests/unit/facts/test_get_chassis_inventory.py +++ b/tests/unit/facts/test_get_chassis_inventory.py @@ -2,7 +2,7 @@ __credits__ = "Jeremy Schulman, Nitin Kumar" import unittest -from nose.plugins.attrib import attr +import nose2 from mock import patch, MagicMock import os @@ -12,7 +12,6 @@ from ncclient.transport import SSHSession -@attr("unit") class TestChassis(unittest.TestCase): @patch("ncclient.manager.connect") def setUp(self, mock_connect): diff --git a/tests/unit/facts/test_get_route_engine_information.py b/tests/unit/facts/test_get_route_engine_information.py index ca26bf8a6..51c292559 100644 --- a/tests/unit/facts/test_get_route_engine_information.py +++ b/tests/unit/facts/test_get_route_engine_information.py @@ -2,7 +2,7 @@ __credits__ = "Jeremy Schulman, Nitin Kumar" import unittest -from nose.plugins.attrib import attr +import nose2 from mock import patch, MagicMock import os from lxml import etree @@ -13,7 +13,6 @@ from ncclient.transport import SSHSession -@attr("unit") class TestGetRouteEngineInformation(unittest.TestCase): @patch("ncclient.manager.connect") def setUp(self, mock_connect): diff --git a/tests/unit/facts/test_get_software_information.py b/tests/unit/facts/test_get_software_information.py index 2b285dfb3..6e455da2b 100644 --- a/tests/unit/facts/test_get_software_information.py +++ b/tests/unit/facts/test_get_software_information.py @@ -2,7 +2,7 @@ __credits__ = "Jeremy Schulman, Nitin Kumar" import unittest -from nose.plugins.attrib import attr +import nose2 from mock import patch, MagicMock import os from lxml import etree @@ -14,7 +14,6 @@ from ncclient.transport import SSHSession -@attr("unit") class TestGetSoftwareInformation(unittest.TestCase): @patch("ncclient.manager.connect") def setUp(self, mock_connect): diff --git a/tests/unit/facts/test_get_virtual_chassis_information.py b/tests/unit/facts/test_get_virtual_chassis_information.py index fd85de956..2f13ccab1 100644 --- a/tests/unit/facts/test_get_virtual_chassis_information.py +++ b/tests/unit/facts/test_get_virtual_chassis_information.py @@ -2,7 +2,7 @@ __credits__ = "Jeremy Schulman, Nitin Kumar" import unittest -from nose.plugins.attrib import attr +import nose2 from mock import patch, MagicMock import os import sys @@ -15,7 +15,6 @@ from ncclient.transport import SSHSession -@attr("unit") class TestGetVirtualChassisInformation(unittest.TestCase): @patch("ncclient.manager.connect") def setUp(self, mock_connect): diff --git a/tests/unit/facts/test_ifd_style.py b/tests/unit/facts/test_ifd_style.py index 7bcbd02d3..ddcfe48de 100644 --- a/tests/unit/facts/test_ifd_style.py +++ b/tests/unit/facts/test_ifd_style.py @@ -2,7 +2,7 @@ __credits__ = "Jeremy Schulman, Nitin Kumar" import unittest -from nose.plugins.attrib import attr +import nose2 from mock import patch, MagicMock import os from lxml import etree @@ -14,7 +14,6 @@ from ncclient.transport import SSHSession -@attr("unit") class TestIfdStyle(unittest.TestCase): @patch("ncclient.manager.connect") def setUp(self, mock_connect): diff --git a/tests/unit/facts/test_iri_mapping.py b/tests/unit/facts/test_iri_mapping.py index 1d73e5258..66ba6ca44 100644 --- a/tests/unit/facts/test_iri_mapping.py +++ b/tests/unit/facts/test_iri_mapping.py @@ -2,7 +2,7 @@ __credits__ = "Jeremy Schulman, Nitin Kumar" import unittest -from nose.plugins.attrib import attr +import nose2 from mock import patch, MagicMock import os @@ -12,7 +12,6 @@ from ncclient.transport import SSHSession -@attr("unit") class TestIriMapping(unittest.TestCase): @patch("ncclient.manager.connect") def setUp(self, mock_connect): diff --git a/tests/unit/facts/test_personality.py b/tests/unit/facts/test_personality.py index 59de7118e..e152777d2 100644 --- a/tests/unit/facts/test_personality.py +++ b/tests/unit/facts/test_personality.py @@ -2,7 +2,7 @@ __credits__ = "Jeremy Schulman, Nitin Kumar" import unittest -from nose.plugins.attrib import attr +import nose2 from mock import patch, MagicMock import os from jnpr.junos.exception import RpcError @@ -13,7 +13,6 @@ from ncclient.transport import SSHSession -@attr("unit") class TestPersonality(unittest.TestCase): @patch("ncclient.manager.connect") def setUp(self, mock_connect): diff --git a/tests/unit/facts/test_swver.py b/tests/unit/facts/test_swver.py index b9ad9edb5..a1a2dac14 100644 --- a/tests/unit/facts/test_swver.py +++ b/tests/unit/facts/test_swver.py @@ -7,12 +7,11 @@ import unittest2 as unittest except: import unittest -from nose.plugins.attrib import attr +import nose2 from jnpr.junos.facts.swver import version_info, get_facts -@attr("unit") class TestVersionInfo(unittest.TestCase): if six.PY2: assertCountEqual = unittest.TestCase.assertItemsEqual diff --git a/tests/unit/ofacts/test_chassis.py b/tests/unit/ofacts/test_chassis.py index 5ca068f56..d3f9beb62 100644 --- a/tests/unit/ofacts/test_chassis.py +++ b/tests/unit/ofacts/test_chassis.py @@ -2,7 +2,7 @@ __credits__ = "Jeremy Schulman" import unittest -from nose.plugins.attrib import attr +import nose2 from mock import patch, MagicMock from lxml import etree import os @@ -16,7 +16,6 @@ from ncclient.transport import SSHSession -@attr("unit") class TestChassis(unittest.TestCase): @patch("ncclient.manager.connect") @patch("jnpr.junos.device.warnings") diff --git a/tests/unit/ofacts/test_domain.py b/tests/unit/ofacts/test_domain.py index 1b6c7a50e..87c303432 100644 --- a/tests/unit/ofacts/test_domain.py +++ b/tests/unit/ofacts/test_domain.py @@ -2,7 +2,7 @@ __credits__ = "Jeremy Schulman" import unittest -from nose.plugins.attrib import attr +import nose2 from mock import patch, MagicMock from lxml import etree @@ -11,7 +11,6 @@ from jnpr.junos.exception import RpcError -@attr("unit") class TestDomain(unittest.TestCase): @patch("ncclient.manager.connect") @patch("jnpr.junos.device.warnings") diff --git a/tests/unit/ofacts/test_ifd_style.py b/tests/unit/ofacts/test_ifd_style.py index 1e97e58d1..d9ca28387 100644 --- a/tests/unit/ofacts/test_ifd_style.py +++ b/tests/unit/ofacts/test_ifd_style.py @@ -3,13 +3,12 @@ import unittest from mock import patch -from nose.plugins.attrib import attr +import nose2 from jnpr.junos import Device from jnpr.junos.ofacts.ifd_style import facts_ifd_style as ifd_style -@attr("unit") class TestIFDStyle(unittest.TestCase): @patch("jnpr.junos.device.warnings") def setUp(self, mock_warnings): diff --git a/tests/unit/ofacts/test_personality.py b/tests/unit/ofacts/test_personality.py index 38cd84f36..c37ee504c 100644 --- a/tests/unit/ofacts/test_personality.py +++ b/tests/unit/ofacts/test_personality.py @@ -3,13 +3,12 @@ import unittest from mock import patch -from nose.plugins.attrib import attr +import nose2 from jnpr.junos import Device from jnpr.junos.ofacts.personality import facts_personality as personality -@attr("unit") class TestPersonality(unittest.TestCase): @patch("jnpr.junos.device.warnings") def setUp(self, mock_warnings): diff --git a/tests/unit/ofacts/test_routing_engines.py b/tests/unit/ofacts/test_routing_engines.py index 4fbce8dcd..a7f068205 100644 --- a/tests/unit/ofacts/test_routing_engines.py +++ b/tests/unit/ofacts/test_routing_engines.py @@ -2,7 +2,7 @@ __credits__ = "Jeremy Schulman" import unittest -from nose.plugins.attrib import attr +import nose2 from mock import patch, MagicMock import os import sys @@ -14,7 +14,6 @@ from ncclient.transport import SSHSession -@attr("unit") class TestRoutingEngines(unittest.TestCase): @patch("ncclient.manager.connect") @patch("jnpr.junos.device.warnings") diff --git a/tests/unit/ofacts/test_srx_cluster.py b/tests/unit/ofacts/test_srx_cluster.py index 7508a50cf..ce07983b4 100644 --- a/tests/unit/ofacts/test_srx_cluster.py +++ b/tests/unit/ofacts/test_srx_cluster.py @@ -2,7 +2,7 @@ __credits__ = "Jeremy Schulman" import unittest -from nose.plugins.attrib import attr +import nose2 from mock import patch import os @@ -13,7 +13,6 @@ from ncclient.transport import SSHSession -@attr("unit") class TestSrxCluster(unittest.TestCase): @patch("ncclient.manager.connect") @patch("jnpr.junos.device.warnings") diff --git a/tests/unit/ofacts/test_switch_style.py b/tests/unit/ofacts/test_switch_style.py index 3deef0520..83dfcb5a8 100644 --- a/tests/unit/ofacts/test_switch_style.py +++ b/tests/unit/ofacts/test_switch_style.py @@ -3,13 +3,12 @@ import unittest from mock import patch -from nose.plugins.attrib import attr +import nose2 from jnpr.junos import Device from jnpr.junos.ofacts.switch_style import facts_switch_style as switch_style -@attr("unit") class TestSwitchStyle(unittest.TestCase): @patch("jnpr.junos.device.warnings") def setUp(self, mock_warnings): diff --git a/tests/unit/ofacts/test_swver.py b/tests/unit/ofacts/test_swver.py index ba306e59f..59e52fa07 100644 --- a/tests/unit/ofacts/test_swver.py +++ b/tests/unit/ofacts/test_swver.py @@ -5,7 +5,7 @@ import unittest2 as unittest except ImportError: import unittest -from nose.plugins.attrib import attr +import nose2 from mock import patch, MagicMock import os @@ -17,7 +17,6 @@ from jnpr.junos.exception import RpcError -@attr("unit") class TestSwver(unittest.TestCase): @patch("ncclient.manager.connect") @patch("jnpr.junos.device.warnings") diff --git a/tests/unit/test_console.py b/tests/unit/test_console.py index 7a1a29029..3b8769715 100644 --- a/tests/unit/test_console.py +++ b/tests/unit/test_console.py @@ -3,7 +3,7 @@ except ImportError: import unittest from jnpr.junos.utils.config import Config -from nose.plugins.attrib import attr +import nose2 from mock import patch, MagicMock, call import re import sys @@ -23,8 +23,17 @@ builtin_string = "builtins" -@attr("unit") class TestConsole(unittest.TestCase): + @classmethod + def setUpClass(cls): + # Save object state + cls.open = tty_netconf.open + + @classmethod + def tearDownClass(cls): + # Revert object state + tty_netconf.open = cls.open + @patch("jnpr.junos.transport.tty_telnet.Telnet._tty_open") @patch("jnpr.junos.transport.tty_telnet.telnetlib.Telnet.expect") @patch("jnpr.junos.transport.tty_telnet.Telnet.write") diff --git a/tests/unit/test_decorators.py b/tests/unit/test_decorators.py index 5eb102666..aefde05e4 100644 --- a/tests/unit/test_decorators.py +++ b/tests/unit/test_decorators.py @@ -2,7 +2,7 @@ import unittest2 as unittest except ImportError: import unittest -from nose.plugins.attrib import attr +import nose2 from lxml.etree import XML @@ -22,7 +22,6 @@ __author__ = "Rick Sherman" -@attr("unit") class Test_Decorators(unittest.TestCase): @patch("ncclient.manager.connect") def setUp(self, mock_connect): diff --git a/tests/unit/test_device.py b/tests/unit/test_device.py index 2a3f9a0a2..3c8c6604f 100644 --- a/tests/unit/test_device.py +++ b/tests/unit/test_device.py @@ -2,7 +2,7 @@ import unittest2 as unittest except ImportError: import unittest -from nose.plugins.attrib import attr +import nose2 from mock import MagicMock, patch, mock_open, call import os from lxml import etree @@ -53,7 +53,6 @@ } -@attr("unit") class Test_MyTemplateLoader(unittest.TestCase): def setUp(self): from jnpr.junos.device import _MyTemplateLoader @@ -78,7 +77,6 @@ def test_temp_load_get_source_filter_true(self, os_path_mock): self.template_loader.get_source(None, None) -@attr("unit") class TestDevice(unittest.TestCase): @patch("ncclient.manager.connect") def setUp(self, mock_connect): @@ -420,6 +418,7 @@ def test_device_local(self): Device.ON_JUNOS = True localdev = Device() self.assertEqual(localdev._hostname, "localhost") + Device.ON_JUNOS = False @patch("jnpr.junos.device.os") @patch(builtin_string + ".open") @@ -475,7 +474,9 @@ def test_device_open_with_look_for_keys_False(self, mock_connect, mock_execute): """ mock_connect.side_effect = self._mock_manager mock_execute.side_effect = self._mock_manager - self.dev2 = Device(host="2.2.2.2", user="test", password="password123", look_for_keys=False) + self.dev2 = Device( + host="2.2.2.2", user="test", password="password123", look_for_keys=False + ) self.dev2.open() self.assertEqual(self.dev2.connected, True) @@ -490,7 +491,9 @@ def test_device_open_with_look_for_keys_True(self, mock_connect, mock_execute): """ mock_connect.side_effect = self._mock_manager mock_execute.side_effect = self._mock_manager - self.dev2 = Device(host="2.2.2.2", user="test", password="password123", look_for_keys=True) + self.dev2 = Device( + host="2.2.2.2", user="test", password="password123", look_for_keys=True + ) self.dev2.open() self.assertEqual(self.dev2.connected, True) diff --git a/tests/unit/test_exception.py b/tests/unit/test_exception.py index 65ad7a164..ad210a0de 100644 --- a/tests/unit/test_exception.py +++ b/tests/unit/test_exception.py @@ -1,5 +1,5 @@ import unittest -from nose.plugins.attrib import attr +import nose2 from jnpr.junos.exception import ( RpcError, CommitError, @@ -81,7 +81,6 @@ }""" -@attr("unit") class Test_RpcError(unittest.TestCase): def test_rpcerror_repr(self): rsp = etree.XML(rpc_xml) diff --git a/tests/unit/test_factcache.py b/tests/unit/test_factcache.py index 89270dbcc..b173c39dd 100644 --- a/tests/unit/test_factcache.py +++ b/tests/unit/test_factcache.py @@ -2,7 +2,7 @@ import unittest2 as unittest except ImportError: import unittest -from nose.plugins.attrib import attr +import nose2 from mock import patch, MagicMock, call from jnpr.junos.exception import FactLoopError @@ -15,7 +15,6 @@ __credits__ = "Jeremy Schulman, Nitin Kumar" -@attr("unit") class TestFactCache(unittest.TestCase): @patch("ncclient.manager.connect") def setUp(self, mock_connect): diff --git a/tests/unit/test_junos.py b/tests/unit/test_junos.py index 77c5d6ff9..ca1bde283 100644 --- a/tests/unit/test_junos.py +++ b/tests/unit/test_junos.py @@ -3,17 +3,17 @@ import unittest import sys -from nose.plugins.attrib import attr +import nose2 from mock import patch __author__ = "Nitin Kumar" __credits__ = "Jeremy Schulman" -@attr("unit") class TestJunosInit(unittest.TestCase): def test_warning(self): - with patch.object(sys.modules["sys"], "version_info", (2, 6, 3)) as mock_sys: + print(sys.modules["sys"].version_info) + with patch.object(sys.modules["sys"], "version_info", (3, 8, 0)) as mock_sys: from jnpr import junos - self.assertEqual(mock_sys, (2, 6, 3)) + self.assertEqual(mock_sys, (3, 8, 0)) diff --git a/tests/unit/test_jxml.py b/tests/unit/test_jxml.py index 725bd7201..de4b893e4 100644 --- a/tests/unit/test_jxml.py +++ b/tests/unit/test_jxml.py @@ -1,7 +1,7 @@ import os import unittest from io import StringIO -from nose.plugins.attrib import attr +import nose2 from mock import patch from jnpr.junos.jxml import ( NAME, @@ -17,7 +17,6 @@ __credits__ = "Jeremy Schulman" -@attr("unit") class Test_JXML(unittest.TestCase): def test_name(self): op = NAME("test") @@ -28,7 +27,7 @@ def test_insert(self): self.assertEqual(op["insert"], "test") def test_remove_namespaces(self): - xmldata = u""" + xmldata = """ @@ -57,14 +56,14 @@ def test_cscript_conf_return_none(self, dev_handler): self.assertTrue(op is None) def test_cscript_conf_output_tag_child_element(self): - xmldata = u""" + xmldata = """ shutdown: [pid 8683] Shutdown NOW! """ - xmldata_without_ns = u""" + xmldata_without_ns = """ shutdown: [pid 8683] Shutdown NOW! @@ -75,7 +74,7 @@ def test_cscript_conf_output_tag_child_element(self): self.assertEqual(str(rpc_reply), xmldata_without_ns) def test_cscript_conf_output_tag_not_first_child_element(self): - xmldata = u""" + xmldata = """ shutdown: [pid 8683] @@ -84,7 +83,7 @@ def test_cscript_conf_output_tag_not_first_child_element(self): """ - xmldata_without_ns = u""" + xmldata_without_ns = """ shutdown: [pid 8683] Shutdown NOW! diff --git a/tests/unit/test_rpcmeta.py b/tests/unit/test_rpcmeta.py index 4aa2cd53f..7b4f0579e 100644 --- a/tests/unit/test_rpcmeta.py +++ b/tests/unit/test_rpcmeta.py @@ -1,7 +1,7 @@ import unittest import os import re -from nose.plugins.attrib import attr +import nose2 from jnpr.junos.device import Device from jnpr.junos.rpcmeta import _RpcMetaExec @@ -17,7 +17,6 @@ __credits__ = "Jeremy Schulman" -@attr("unit") class Test_RpcMetaExec(unittest.TestCase): @patch("ncclient.manager.connect") def setUp(self, mock_connect): @@ -132,7 +131,7 @@ def test_rpcmeta_exec_rpc_format_json_14_2(self): op["system-users-information"][0]["uptime-information"][0]["date-time"][0][ "data" ], - u"4:43AM", + "4:43AM", ) def test_rpcmeta_exec_rpc_format_json_gt_14_2(self): @@ -143,7 +142,7 @@ def test_rpcmeta_exec_rpc_format_json_gt_14_2(self): op["system-users-information"][0]["uptime-information"][0]["date-time"][0][ "data" ], - u"4:43AM", + "4:43AM", ) @patch("jnpr.junos.device.warnings") diff --git a/tests/unit/transport/test_serial.py b/tests/unit/transport/test_serial.py index 075d3a5fb..570213baf 100644 --- a/tests/unit/transport/test_serial.py +++ b/tests/unit/transport/test_serial.py @@ -2,7 +2,7 @@ import unittest2 as unittest except ImportError: import unittest -from nose.plugins.attrib import attr +import nose2 from mock import MagicMock, patch import sys import six @@ -15,7 +15,6 @@ builtin_string = "builtins" -@attr("unit") class TestSerial(unittest.TestCase): @patch("jnpr.junos.transport.tty_serial.serial.Serial.open") @patch("jnpr.junos.transport.tty_serial.serial.Serial.write") @@ -77,7 +76,6 @@ def test_tty_serial_read_prompt(self): self.assertEqual(self.dev._tty.read_prompt()[0], None) -@attr("unit") class TestSerialWin(unittest.TestCase): @patch("jnpr.junos.transport.tty_serial.serial.Serial.open") @patch("jnpr.junos.transport.tty_serial.serial.Serial.read") diff --git a/tests/unit/transport/test_tty.py b/tests/unit/transport/test_tty.py index 88129c071..87f1c2b51 100644 --- a/tests/unit/transport/test_tty.py +++ b/tests/unit/transport/test_tty.py @@ -5,14 +5,13 @@ except ImportError: import unittest -from nose.plugins.attrib import attr +import nose2 from mock import MagicMock, patch from jnpr.junos.transport.tty import Terminal from jnpr.junos import exception as EzErrors -@attr("unit") class TestTTY(unittest.TestCase): def setUp(self): logging.getLogger("jnpr.junos.tty") diff --git a/tests/unit/transport/test_tty_netconf.py b/tests/unit/transport/test_tty_netconf.py index 8b8e1dd4f..612d0164a 100644 --- a/tests/unit/transport/test_tty_netconf.py +++ b/tests/unit/transport/test_tty_netconf.py @@ -2,9 +2,11 @@ import unittest2 as unittest except ImportError: import unittest -from nose.plugins.attrib import attr +import nose2 from mock import MagicMock, patch + from jnpr.junos.transport.tty_netconf import tty_netconf + import six import os import select @@ -12,7 +14,6 @@ from ncclient.operations import RPCError -@attr("unit") class TestTTYNetconf(unittest.TestCase): def setUp(self): self.tty_net = tty_netconf(MagicMock()) diff --git a/tests/unit/transport/test_tty_ssh.py b/tests/unit/transport/test_tty_ssh.py index 09d9cc526..62729c7ac 100644 --- a/tests/unit/transport/test_tty_ssh.py +++ b/tests/unit/transport/test_tty_ssh.py @@ -5,12 +5,11 @@ import unittest2 as unittest except ImportError: import unittest -from nose.plugins.attrib import attr +import nose2 from mock import MagicMock, patch from jnpr.junos.transport.tty_ssh import SSH -@attr("unit") class TestTTYSSH(unittest.TestCase): @patch("jnpr.junos.transport.tty_ssh.paramiko") def setUp(self, mock_paramiko): diff --git a/tests/unit/transport/test_tty_telnet.py b/tests/unit/transport/test_tty_telnet.py index e34c34578..2030a61ff 100644 --- a/tests/unit/transport/test_tty_telnet.py +++ b/tests/unit/transport/test_tty_telnet.py @@ -4,13 +4,12 @@ import unittest2 as unittest except ImportError: import unittest -from nose.plugins.attrib import attr +import nose2 from mock import MagicMock, patch from jnpr.junos.transport.tty_telnet import Telnet import six -@attr("unit") class TestTTYTelnet(unittest.TestCase): @patch("jnpr.junos.transport.tty_telnet.telnetlib.Telnet") def setUp(self, mpock_telnet): diff --git a/tests/unit/utils/test_config.py b/tests/unit/utils/test_config.py index 72a37edd6..2dbd69101 100644 --- a/tests/unit/utils/test_config.py +++ b/tests/unit/utils/test_config.py @@ -1,6 +1,6 @@ import unittest import sys -from nose.plugins.attrib import attr +import nose2 from jnpr.junos import Device from jnpr.junos.utils.config import Config @@ -32,7 +32,6 @@ builtin_string = "builtins" -@attr("unit") class TestConfig(unittest.TestCase): @patch("ncclient.manager.connect") def setUp(self, mock_connect): diff --git a/tests/unit/utils/test_fs.py b/tests/unit/utils/test_fs.py index 0cc8eefc0..bc03e623b 100644 --- a/tests/unit/utils/test_fs.py +++ b/tests/unit/utils/test_fs.py @@ -1,5 +1,5 @@ import unittest -from nose.plugins.attrib import attr +import nose2 import os from ncclient.manager import Manager, make_device_handler @@ -16,7 +16,6 @@ __credits__ = "Jeremy Schulman" -@attr("unit") class TestFS(unittest.TestCase): @patch("ncclient.manager.connect") def setUp(self, mock_connect): diff --git a/tests/unit/utils/test_ftp.py b/tests/unit/utils/test_ftp.py index ab3807236..fcc0606e5 100644 --- a/tests/unit/utils/test_ftp.py +++ b/tests/unit/utils/test_ftp.py @@ -1,5 +1,5 @@ import unittest -from nose.plugins.attrib import attr +import nose2 import ftplib import sys import os @@ -15,7 +15,6 @@ builtin_string = "builtins" -@attr("unit") @unittest.skipIf(sys.platform == "win32", "will work for windows in coming days") class TestFtp(unittest.TestCase): @patch("ftplib.FTP.connect") diff --git a/tests/unit/utils/test_scp.py b/tests/unit/utils/test_scp.py index 66489e311..517e802ca 100644 --- a/tests/unit/utils/test_scp.py +++ b/tests/unit/utils/test_scp.py @@ -3,7 +3,7 @@ from contextlib import contextmanager import unittest -from nose.plugins.attrib import attr +import nose2 from jnpr.junos import Device from jnpr.junos.utils.scp import SCP @@ -19,7 +19,6 @@ builtin_string = "builtins" -@attr("unit") class TestScp(unittest.TestCase): def setUp(self): self.dev = Device(host="1.1.1.1") diff --git a/tests/unit/utils/test_start_shell.py b/tests/unit/utils/test_start_shell.py index 49642b357..0034f2b3e 100644 --- a/tests/unit/utils/test_start_shell.py +++ b/tests/unit/utils/test_start_shell.py @@ -1,5 +1,5 @@ import unittest -from nose.plugins.attrib import attr +import nose2 from jnpr.junos import Device from jnpr.junos.utils.start_shell import StartShell @@ -10,7 +10,6 @@ __credits__ = "Jeremy Schulman, Nitin Kumar" -@attr("unit") class TestStartShell(unittest.TestCase): @patch("paramiko.SSHClient") def setUp(self, mock_connect): @@ -70,7 +69,7 @@ def test_startshell_wait_for_regex(self, mock_select): ---(more)--- """ self.assertTrue( - self.shell.wait_for("---\(more\s?\d*%?\)---\n\s*|%")[0] + str(self.shell.wait_for("---\(more\s?\d*%?\)---\n\s*|%")[0]) in self.shell._chan.recv.return_value ) diff --git a/tests/unit/utils/test_sw.py b/tests/unit/utils/test_sw.py index 113cd02e6..1f669b078 100644 --- a/tests/unit/utils/test_sw.py +++ b/tests/unit/utils/test_sw.py @@ -7,7 +7,7 @@ import unittest2 as unittest except ImportError: import unittest -from nose.plugins.attrib import attr +import nose2 from contextlib import contextmanager from jnpr.junos import Device from jnpr.junos.exception import RpcError, SwRollbackError, RpcTimeoutError @@ -59,7 +59,6 @@ } -@attr("unit") class TestSW(unittest.TestCase): @patch("ncclient.manager.connect") def setUp(self, mock_connect): @@ -676,25 +675,25 @@ def test_sw_install_multi_vc(self, mock_pkgadd): @patch("jnpr.junos.utils.sw.SW.pkgadd") def test_sw_install_multi_vc_member_id(self, mock_pkgadd): mock_pkgadd.return_value = True, "msg" - self.dev.facts["vc_master"] = '0' + self.dev.facts["vc_master"] = "0" self.sw._multi_RE = True self.sw._multi_VC = True self.sw._RE_list = ("version_RE0", "version_RE1") - self.assertTrue(self.sw.install("file", member_id=['1'], no_copy=True)[0]) + self.assertTrue(self.sw.install("file", member_id=["1"], no_copy=True)[0]) @patch("jnpr.junos.utils.sw.SW.pkgadd") def test_sw_install_multi_vc_multiple_member_id(self, mock_pkgadd): mock_pkgadd.return_value = True, "msg" - self.dev.facts["vc_master"] = '0' + self.dev.facts["vc_master"] = "0" self.sw._multi_RE = False self.sw._multi_VC_nsync = True self.sw._RE_list = ("version_RE0", "version_RE1") - self.assertTrue(self.sw.install("file", member_id=['0','1'], no_copy=True)[0]) + self.assertTrue(self.sw.install("file", member_id=["0", "1"], no_copy=True)[0]) @patch("jnpr.junos.utils.sw.SW.pkgadd") def test_sw_install_mixed_vc(self, mock_pkgadd): mock_pkgadd.return_value = True - self.dev.facts["vc_master"] = '0' + self.dev.facts["vc_master"] = "0" self.sw._mixed_VC = True self.sw._RE_list = ("version_RE0", "version_RE1") self.assertTrue(self.sw.install(pkg_set=["abc.tgz", "pqr.tgz"], no_copy=True)) diff --git a/tests/unit/utils/test_util.py b/tests/unit/utils/test_util.py index 8e6a267ab..86f5cb644 100644 --- a/tests/unit/utils/test_util.py +++ b/tests/unit/utils/test_util.py @@ -2,7 +2,7 @@ __credits__ = "Jeremy Schulman" import unittest -from nose.plugins.attrib import attr +import nose2 from jnpr.junos import Device from jnpr.junos.utils.util import Util @@ -10,7 +10,6 @@ from mock import patch -@attr("unit") class TestUtil(unittest.TestCase): @patch("ncclient.manager.connect") def setUp(self, mock_connect): diff --git a/versioneer.py b/versioneer.py index 2b5454051..de97d9042 100644 --- a/versioneer.py +++ b/versioneer.py @@ -1,4 +1,4 @@ -# Version: 0.18 +# Version: 0.29 """The Versioneer - like a rocketeer, but for versions. @@ -6,18 +6,14 @@ ============== * like a rocketeer, but for versions! -* https://github.com/warner/python-versioneer +* https://github.com/python-versioneer/python-versioneer * Brian Warner -* License: Public Domain -* Compatible With: python2.6, 2.7, 3.2, 3.3, 3.4, 3.5, 3.6, and pypy -* [![Latest Version] -(https://pypip.in/version/versioneer/badge.svg?style=flat) -](https://pypi.python.org/pypi/versioneer/) -* [![Build Status] -(https://travis-ci.org/warner/python-versioneer.png?branch=master) -](https://travis-ci.org/warner/python-versioneer) - -This is a tool for managing a recorded version number in distutils-based +* License: Public Domain (Unlicense) +* Compatible with: Python 3.7, 3.8, 3.9, 3.10, 3.11 and pypy3 +* [![Latest Version][pypi-image]][pypi-url] +* [![Build Status][travis-image]][travis-url] + +This is a tool for managing a recorded version number in setuptools-based python projects. The goal is to remove the tedious and error-prone "update the embedded version string" step from your release process. Making a new release should be as easy as recording a new tag in your version-control @@ -26,9 +22,38 @@ ## Quick Install -* `pip install versioneer` to somewhere to your $PATH -* add a `[versioneer]` section to your setup.cfg (see below) -* run `versioneer install` in your source tree, commit the results +Versioneer provides two installation modes. The "classic" vendored mode installs +a copy of versioneer into your repository. The experimental build-time dependency mode +is intended to allow you to skip this step and simplify the process of upgrading. + +### Vendored mode + +* `pip install versioneer` to somewhere in your $PATH + * A [conda-forge recipe](https://github.com/conda-forge/versioneer-feedstock) is + available, so you can also use `conda install -c conda-forge versioneer` +* add a `[tool.versioneer]` section to your `pyproject.toml` or a + `[versioneer]` section to your `setup.cfg` (see [Install](INSTALL.md)) + * Note that you will need to add `tomli; python_version < "3.11"` to your + build-time dependencies if you use `pyproject.toml` +* run `versioneer install --vendor` in your source tree, commit the results +* verify version information with `python setup.py version` + +### Build-time dependency mode + +* `pip install versioneer` to somewhere in your $PATH + * A [conda-forge recipe](https://github.com/conda-forge/versioneer-feedstock) is + available, so you can also use `conda install -c conda-forge versioneer` +* add a `[tool.versioneer]` section to your `pyproject.toml` or a + `[versioneer]` section to your `setup.cfg` (see [Install](INSTALL.md)) +* add `versioneer` (with `[toml]` extra, if configuring in `pyproject.toml`) + to the `requires` key of the `build-system` table in `pyproject.toml`: + ```toml + [build-system] + requires = ["setuptools", "versioneer[toml]"] + build-backend = "setuptools.build_meta" + ``` +* run `versioneer install --no-vendor` in your source tree, commit the results +* verify version information with `python setup.py version` ## Version Identifiers @@ -60,7 +85,7 @@ for example `git describe --tags --dirty --always` reports things like "0.7-1-g574ab98-dirty" to indicate that the checkout is one revision past the 0.7 tag, has a unique revision id of "574ab98", and is "dirty" (it has -uncommitted changes. +uncommitted changes). The version identifier is used for multiple purposes: @@ -165,7 +190,7 @@ Some situations are known to cause problems for Versioneer. This details the most significant ones. More can be found on Github -[issues page](https://github.com/warner/python-versioneer/issues). +[issues page](https://github.com/python-versioneer/python-versioneer/issues). ### Subprojects @@ -179,7 +204,7 @@ `setup.cfg`, and `tox.ini`. Projects like these produce multiple PyPI distributions (and upload multiple independently-installable tarballs). * Source trees whose main purpose is to contain a C library, but which also - provide bindings to Python (and perhaps other langauges) in subdirectories. + provide bindings to Python (and perhaps other languages) in subdirectories. Versioneer will look for `.git` in parent directories, and most operations should get the right version string. However `pip` and `setuptools` have bugs @@ -193,9 +218,9 @@ Pip-8.1.1 is known to have this problem, but hopefully it will get fixed in some later version. -[Bug #38](https://github.com/warner/python-versioneer/issues/38) is tracking +[Bug #38](https://github.com/python-versioneer/python-versioneer/issues/38) is tracking this issue. The discussion in -[PR #61](https://github.com/warner/python-versioneer/pull/61) describes the +[PR #61](https://github.com/python-versioneer/python-versioneer/pull/61) describes the issue from the Versioneer side in more detail. [pip PR#3176](https://github.com/pypa/pip/pull/3176) and [pip PR#3615](https://github.com/pypa/pip/pull/3615) contain work to improve @@ -223,31 +248,20 @@ cause egg_info to be rebuilt (including `sdist`, `wheel`, and installing into a different virtualenv), so this can be surprising. -[Bug #83](https://github.com/warner/python-versioneer/issues/83) describes +[Bug #83](https://github.com/python-versioneer/python-versioneer/issues/83) describes this one, but upgrading to a newer version of setuptools should probably resolve it. -### Unicode version strings - -While Versioneer works (and is continually tested) with both Python 2 and -Python 3, it is not entirely consistent with bytes-vs-unicode distinctions. -Newer releases probably generate unicode version strings on py2. It's not -clear that this is wrong, but it may be surprising for applications when then -write these strings to a network connection or include them in bytes-oriented -APIs like cryptographic checksums. - -[Bug #71](https://github.com/warner/python-versioneer/issues/71) investigates -this question. - ## Updating Versioneer To upgrade your project to a new release of Versioneer, do the following: * install the new Versioneer (`pip install -U versioneer` or equivalent) -* edit `setup.cfg`, if necessary, to include any new configuration settings - indicated by the release notes. See [UPGRADING](./UPGRADING.md) for details. -* re-run `versioneer install` in your source tree, to replace +* edit `setup.cfg` and `pyproject.toml`, if necessary, + to include any new configuration settings indicated by the release notes. + See [UPGRADING](./UPGRADING.md) for details. +* re-run `versioneer install --[no-]vendor` in your source tree, to replace `SRC/_version.py` * commit any changed files @@ -264,36 +278,70 @@ direction and include code from all supported VCS systems, reducing the number of intermediate scripts. +## Similar projects + +* [setuptools_scm](https://github.com/pypa/setuptools_scm/) - a non-vendored build-time + dependency +* [minver](https://github.com/jbweston/miniver) - a lightweight reimplementation of + versioneer +* [versioningit](https://github.com/jwodder/versioningit) - a PEP 518-based setuptools + plugin ## License To make Versioneer easier to embed, all its code is dedicated to the public domain. The `_version.py` that it creates is also in the public domain. -Specifically, both are released under the Creative Commons "Public Domain -Dedication" license (CC0-1.0), as described in -https://creativecommons.org/publicdomain/zero/1.0/ . +Specifically, both are released under the "Unlicense", as described in +https://unlicense.org/. -""" +[pypi-image]: https://img.shields.io/pypi/v/versioneer.svg +[pypi-url]: https://pypi.python.org/pypi/versioneer/ +[travis-image]: +https://img.shields.io/travis/com/python-versioneer/python-versioneer.svg +[travis-url]: https://travis-ci.com/github/python-versioneer/python-versioneer -from __future__ import print_function +""" +# pylint:disable=invalid-name,import-outside-toplevel,missing-function-docstring +# pylint:disable=missing-class-docstring,too-many-branches,too-many-statements +# pylint:disable=raise-missing-from,too-many-lines,too-many-locals,import-error +# pylint:disable=too-few-public-methods,redefined-outer-name,consider-using-with +# pylint:disable=attribute-defined-outside-init,too-many-arguments -try: - import configparser -except ImportError: - import ConfigParser as configparser +import configparser import errno import json import os import re import subprocess import sys +from pathlib import Path +from typing import Any, Callable, cast, Dict, List, Optional, Tuple, Union +from typing import NoReturn +import functools + +have_tomllib = True +if sys.version_info >= (3, 11): + import tomllib +else: + try: + import tomli as tomllib + except ImportError: + have_tomllib = False class VersioneerConfig: """Container for Versioneer configuration parameters.""" + VCS: str + style: str + tag_prefix: str + versionfile_source: str + versionfile_build: Optional[str] + parentdir_prefix: Optional[str] + verbose: Optional[bool] + -def get_root(): +def get_root() -> str: """Get the project root directory. We require that all commands are run from the project root, i.e. the @@ -301,13 +349,23 @@ def get_root(): """ root = os.path.realpath(os.path.abspath(os.getcwd())) setup_py = os.path.join(root, "setup.py") + pyproject_toml = os.path.join(root, "pyproject.toml") versioneer_py = os.path.join(root, "versioneer.py") - if not (os.path.exists(setup_py) or os.path.exists(versioneer_py)): + if not ( + os.path.exists(setup_py) + or os.path.exists(pyproject_toml) + or os.path.exists(versioneer_py) + ): # allow 'python path/to/setup.py COMMAND' root = os.path.dirname(os.path.realpath(os.path.abspath(sys.argv[0]))) setup_py = os.path.join(root, "setup.py") + pyproject_toml = os.path.join(root, "pyproject.toml") versioneer_py = os.path.join(root, "versioneer.py") - if not (os.path.exists(setup_py) or os.path.exists(versioneer_py)): + if not ( + os.path.exists(setup_py) + or os.path.exists(pyproject_toml) + or os.path.exists(versioneer_py) + ): err = ( "Versioneer was unable to run the project root directory. " "Versioneer requires setup.py to be executed from " @@ -323,46 +381,64 @@ def get_root(): # module-import table will cache the first one. So we can't use # os.path.dirname(__file__), as that will find whichever # versioneer.py was first imported, even in later projects. - me = os.path.realpath(os.path.abspath(__file__)) - me_dir = os.path.normcase(os.path.splitext(me)[0]) + my_path = os.path.realpath(os.path.abspath(__file__)) + me_dir = os.path.normcase(os.path.splitext(my_path)[0]) vsr_dir = os.path.normcase(os.path.splitext(versioneer_py)[0]) - if me_dir != vsr_dir: + if me_dir != vsr_dir and "VERSIONEER_PEP518" not in globals(): print( "Warning: build in %s is using versioneer.py from %s" - % (os.path.dirname(me), versioneer_py) + % (os.path.dirname(my_path), versioneer_py) ) except NameError: pass return root -def get_config_from_root(root): +def get_config_from_root(root: str) -> VersioneerConfig: """Read the project setup.cfg file to determine Versioneer config.""" - # This might raise EnvironmentError (if setup.cfg is missing), or + # This might raise OSError (if setup.cfg is missing), or # configparser.NoSectionError (if it lacks a [versioneer] section), or # configparser.NoOptionError (if it lacks "VCS="). See the docstring at # the top of versioneer.py for instructions on writing your setup.cfg . - setup_cfg = os.path.join(root, "setup.cfg") - parser = configparser.SafeConfigParser() - with open(setup_cfg, "r") as f: - parser.readfp(f) - VCS = parser.get("versioneer", "VCS") # mandatory - - def get(parser, name): - if parser.has_option("versioneer", name): - return parser.get("versioneer", name) - return None + root_pth = Path(root) + pyproject_toml = root_pth / "pyproject.toml" + setup_cfg = root_pth / "setup.cfg" + section: Union[Dict[str, Any], configparser.SectionProxy, None] = None + if pyproject_toml.exists() and have_tomllib: + try: + with open(pyproject_toml, "rb") as fobj: + pp = tomllib.load(fobj) + section = pp["tool"]["versioneer"] + except (tomllib.TOMLDecodeError, KeyError) as e: + print(f"Failed to load config from {pyproject_toml}: {e}") + print("Try to load it from setup.cfg") + if not section: + parser = configparser.ConfigParser() + with open(setup_cfg) as cfg_file: + parser.read_file(cfg_file) + parser.get("versioneer", "VCS") # raise error if missing + + section = parser["versioneer"] + + # `cast`` really shouldn't be used, but its simplest for the + # common VersioneerConfig users at the moment. We verify against + # `None` values elsewhere where it matters cfg = VersioneerConfig() - cfg.VCS = VCS - cfg.style = get(parser, "style") or "" - cfg.versionfile_source = get(parser, "versionfile_source") - cfg.versionfile_build = get(parser, "versionfile_build") - cfg.tag_prefix = get(parser, "tag_prefix") - if cfg.tag_prefix in ("''", '""'): + cfg.VCS = section["VCS"] + cfg.style = section.get("style", "") + cfg.versionfile_source = cast(str, section.get("versionfile_source")) + cfg.versionfile_build = section.get("versionfile_build") + cfg.tag_prefix = cast(str, section.get("tag_prefix")) + if cfg.tag_prefix in ("''", '""', None): cfg.tag_prefix = "" - cfg.parentdir_prefix = get(parser, "parentdir_prefix") - cfg.verbose = get(parser, "verbose") + cfg.parentdir_prefix = section.get("parentdir_prefix") + if isinstance(section, configparser.SectionProxy): + # Make sure configparser translates to bool + cfg.verbose = section.getboolean("verbose") + else: + cfg.verbose = section.get("verbose") + return cfg @@ -371,41 +447,54 @@ class NotThisMethod(Exception): # these dictionaries contain VCS-specific tools -LONG_VERSION_PY = {} -HANDLERS = {} +LONG_VERSION_PY: Dict[str, str] = {} +HANDLERS: Dict[str, Dict[str, Callable]] = {} -def register_vcs_handler(vcs, method): # decorator - """Decorator to mark a method as the handler for a particular VCS.""" +def register_vcs_handler(vcs: str, method: str) -> Callable: # decorator + """Create decorator to mark a method as the handler of a VCS.""" - def decorate(f): + def decorate(f: Callable) -> Callable: """Store f in HANDLERS[vcs][method].""" - if vcs not in HANDLERS: - HANDLERS[vcs] = {} - HANDLERS[vcs][method] = f + HANDLERS.setdefault(vcs, {})[method] = f return f return decorate -def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False, env=None): +def run_command( + commands: List[str], + args: List[str], + cwd: Optional[str] = None, + verbose: bool = False, + hide_stderr: bool = False, + env: Optional[Dict[str, str]] = None, +) -> Tuple[Optional[str], Optional[int]]: """Call the given command(s).""" assert isinstance(commands, list) - p = None - for c in commands: + process = None + + popen_kwargs: Dict[str, Any] = {} + if sys.platform == "win32": + # This hides the console window if pythonw.exe is used + startupinfo = subprocess.STARTUPINFO() + startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW + popen_kwargs["startupinfo"] = startupinfo + + for command in commands: try: - dispcmd = str([c] + args) + dispcmd = str([command] + args) # remember shell=False, so use git.cmd on windows, not just git - p = subprocess.Popen( - [c] + args, + process = subprocess.Popen( + [command] + args, cwd=cwd, env=env, stdout=subprocess.PIPE, stderr=(subprocess.PIPE if hide_stderr else None), + **popen_kwargs, ) break - except EnvironmentError: - e = sys.exc_info()[1] + except OSError as e: if e.errno == errno.ENOENT: continue if verbose: @@ -416,28 +505,27 @@ def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False, env= if verbose: print("unable to find command, tried %s" % (commands,)) return None, None - stdout = p.communicate()[0].strip() - if sys.version_info[0] >= 3: - stdout = stdout.decode() - if p.returncode != 0: + stdout = process.communicate()[0].strip().decode() + if process.returncode != 0: if verbose: print("unable to run %s (error)" % dispcmd) print("stdout was %s" % stdout) - return None, p.returncode - return stdout, p.returncode + return None, process.returncode + return stdout, process.returncode LONG_VERSION_PY[ "git" -] = ''' +] = r''' # This file helps to compute a version number in source trees obtained from # git-archive tarball (such as those provided by githubs download-from-tag # feature). Distribution tarballs (built by setup.py sdist) and build # directories (produced by setup.py build) will contain a much shorter file # that just contains the computed version number. -# This file is released into the public domain. Generated by -# versioneer-0.18 (https://github.com/warner/python-versioneer) +# This file is released into the public domain. +# Generated by versioneer-0.29 +# https://github.com/python-versioneer/python-versioneer """Git implementation of _version.py.""" @@ -446,9 +534,11 @@ def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False, env= import re import subprocess import sys +from typing import Any, Callable, Dict, List, Optional, Tuple +import functools -def get_keywords(): +def get_keywords() -> Dict[str, str]: """Get the keywords needed to look up the version information.""" # these strings will be replaced by git during git-archive. # setup.py/versioneer.py will grep for the variable names, so they must @@ -464,8 +554,15 @@ def get_keywords(): class VersioneerConfig: """Container for Versioneer configuration parameters.""" + VCS: str + style: str + tag_prefix: str + parentdir_prefix: str + versionfile_source: str + verbose: bool + -def get_config(): +def get_config() -> VersioneerConfig: """Create, populate and return the VersioneerConfig() object.""" # these strings are filled in when 'setup.py versioneer' creates # _version.py @@ -483,13 +580,13 @@ class NotThisMethod(Exception): """Exception raised if a method is not valid for the current scenario.""" -LONG_VERSION_PY = {} -HANDLERS = {} +LONG_VERSION_PY: Dict[str, str] = {} +HANDLERS: Dict[str, Dict[str, Callable]] = {} -def register_vcs_handler(vcs, method): # decorator - """Decorator to mark a method as the handler for a particular VCS.""" - def decorate(f): +def register_vcs_handler(vcs: str, method: str) -> Callable: # decorator + """Create decorator to mark a method as the handler of a VCS.""" + def decorate(f: Callable) -> Callable: """Store f in HANDLERS[vcs][method].""" if vcs not in HANDLERS: HANDLERS[vcs] = {} @@ -498,22 +595,35 @@ def decorate(f): return decorate -def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False, - env=None): +def run_command( + commands: List[str], + args: List[str], + cwd: Optional[str] = None, + verbose: bool = False, + hide_stderr: bool = False, + env: Optional[Dict[str, str]] = None, +) -> Tuple[Optional[str], Optional[int]]: """Call the given command(s).""" assert isinstance(commands, list) - p = None - for c in commands: + process = None + + popen_kwargs: Dict[str, Any] = {} + if sys.platform == "win32": + # This hides the console window if pythonw.exe is used + startupinfo = subprocess.STARTUPINFO() + startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW + popen_kwargs["startupinfo"] = startupinfo + + for command in commands: try: - dispcmd = str([c] + args) + dispcmd = str([command] + args) # remember shell=False, so use git.cmd on windows, not just git - p = subprocess.Popen([c] + args, cwd=cwd, env=env, - stdout=subprocess.PIPE, - stderr=(subprocess.PIPE if hide_stderr - else None)) + process = subprocess.Popen([command] + args, cwd=cwd, env=env, + stdout=subprocess.PIPE, + stderr=(subprocess.PIPE if hide_stderr + else None), **popen_kwargs) break - except EnvironmentError: - e = sys.exc_info()[1] + except OSError as e: if e.errno == errno.ENOENT: continue if verbose: @@ -524,18 +634,20 @@ def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False, if verbose: print("unable to find command, tried %%s" %% (commands,)) return None, None - stdout = p.communicate()[0].strip() - if sys.version_info[0] >= 3: - stdout = stdout.decode() - if p.returncode != 0: + stdout = process.communicate()[0].strip().decode() + if process.returncode != 0: if verbose: print("unable to run %%s (error)" %% dispcmd) print("stdout was %%s" %% stdout) - return None, p.returncode - return stdout, p.returncode + return None, process.returncode + return stdout, process.returncode -def versions_from_parentdir(parentdir_prefix, root, verbose): +def versions_from_parentdir( + parentdir_prefix: str, + root: str, + verbose: bool, +) -> Dict[str, Any]: """Try to determine the version from the parent directory name. Source tarballs conventionally unpack into a directory that includes both @@ -544,15 +656,14 @@ def versions_from_parentdir(parentdir_prefix, root, verbose): """ rootdirs = [] - for i in range(3): + for _ in range(3): dirname = os.path.basename(root) if dirname.startswith(parentdir_prefix): return {"version": dirname[len(parentdir_prefix):], "full-revisionid": None, "dirty": False, "error": None, "date": None} - else: - rootdirs.append(root) - root = os.path.dirname(root) # up a level + rootdirs.append(root) + root = os.path.dirname(root) # up a level if verbose: print("Tried directories %%s but none started with prefix %%s" %% @@ -561,41 +672,48 @@ def versions_from_parentdir(parentdir_prefix, root, verbose): @register_vcs_handler("git", "get_keywords") -def git_get_keywords(versionfile_abs): +def git_get_keywords(versionfile_abs: str) -> Dict[str, str]: """Extract version information from the given file.""" # the code embedded in _version.py can just fetch the value of these # keywords. When used from setup.py, we don't want to import _version.py, # so we do it with a regexp instead. This function is not used from # _version.py. - keywords = {} + keywords: Dict[str, str] = {} try: - f = open(versionfile_abs, "r") - for line in f.readlines(): - if line.strip().startswith("git_refnames ="): - mo = re.search(r'=\s*"(.*)"', line) - if mo: - keywords["refnames"] = mo.group(1) - if line.strip().startswith("git_full ="): - mo = re.search(r'=\s*"(.*)"', line) - if mo: - keywords["full"] = mo.group(1) - if line.strip().startswith("git_date ="): - mo = re.search(r'=\s*"(.*)"', line) - if mo: - keywords["date"] = mo.group(1) - f.close() - except EnvironmentError: + with open(versionfile_abs, "r") as fobj: + for line in fobj: + if line.strip().startswith("git_refnames ="): + mo = re.search(r'=\s*"(.*)"', line) + if mo: + keywords["refnames"] = mo.group(1) + if line.strip().startswith("git_full ="): + mo = re.search(r'=\s*"(.*)"', line) + if mo: + keywords["full"] = mo.group(1) + if line.strip().startswith("git_date ="): + mo = re.search(r'=\s*"(.*)"', line) + if mo: + keywords["date"] = mo.group(1) + except OSError: pass return keywords @register_vcs_handler("git", "keywords") -def git_versions_from_keywords(keywords, tag_prefix, verbose): +def git_versions_from_keywords( + keywords: Dict[str, str], + tag_prefix: str, + verbose: bool, +) -> Dict[str, Any]: """Get version information from git keywords.""" - if not keywords: - raise NotThisMethod("no keywords at all, weird") + if "refnames" not in keywords: + raise NotThisMethod("Short version file found") date = keywords.get("date") if date is not None: + # Use only the last line. Previous lines may contain GPG signature + # information. + date = date.splitlines()[-1] + # git-2.2.0 added "%%cI", which expands to an ISO-8601 -compliant # datestamp. However we prefer "%%ci" (which expands to an "ISO-8601 # -like" string, which we must then edit to make compliant), because @@ -608,11 +726,11 @@ def git_versions_from_keywords(keywords, tag_prefix, verbose): if verbose: print("keywords are unexpanded, not using") raise NotThisMethod("unexpanded keywords, not a git-archive tarball") - refs = set([r.strip() for r in refnames.strip("()").split(",")]) + refs = {r.strip() for r in refnames.strip("()").split(",")} # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of # just "foo-1.0". If we see a "tag: " prefix, prefer those. TAG = "tag: " - tags = set([r[len(TAG):] for r in refs if r.startswith(TAG)]) + tags = {r[len(TAG):] for r in refs if r.startswith(TAG)} if not tags: # Either we're using git < 1.8.3, or there really are no tags. We use # a heuristic: assume all version tags have a digit. The old git %%d @@ -621,7 +739,7 @@ def git_versions_from_keywords(keywords, tag_prefix, verbose): # between branches and tags. By ignoring refnames without digits, we # filter out many common branch names like "release" and # "stabilization", as well as "HEAD" and "master". - tags = set([r for r in refs if re.search(r'\d', r)]) + tags = {r for r in refs if re.search(r'\d', r)} if verbose: print("discarding '%%s', no digits" %% ",".join(refs - tags)) if verbose: @@ -630,6 +748,11 @@ def git_versions_from_keywords(keywords, tag_prefix, verbose): # sorting will prefer e.g. "2.0" over "2.0rc1" if ref.startswith(tag_prefix): r = ref[len(tag_prefix):] + # Filter out refs that exactly match prefix or that don't start + # with a number once the prefix is stripped (mostly a concern + # when prefix is '') + if not re.match(r'\d', r): + continue if verbose: print("picking %%s" %% r) return {"version": r, @@ -645,7 +768,12 @@ def git_versions_from_keywords(keywords, tag_prefix, verbose): @register_vcs_handler("git", "pieces_from_vcs") -def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): +def git_pieces_from_vcs( + tag_prefix: str, + root: str, + verbose: bool, + runner: Callable = run_command +) -> Dict[str, Any]: """Get version from 'git describe' in the root of the source tree. This only gets called if the git-archive 'subst' keywords were *not* @@ -656,8 +784,15 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): if sys.platform == "win32": GITS = ["git.cmd", "git.exe"] - out, rc = run_command(GITS, ["rev-parse", "--git-dir"], cwd=root, - hide_stderr=True) + # GIT_DIR can interfere with correct operation of Versioneer. + # It may be intended to be passed to the Versioneer-versioned project, + # but that should not change where we get our version from. + env = os.environ.copy() + env.pop("GIT_DIR", None) + runner = functools.partial(runner, env=env) + + _, rc = runner(GITS, ["rev-parse", "--git-dir"], cwd=root, + hide_stderr=not verbose) if rc != 0: if verbose: print("Directory %%s not under git control" %% root) @@ -665,24 +800,57 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): # if there is a tag matching tag_prefix, this yields TAG-NUM-gHEX[-dirty] # if there isn't one, this yields HEX[-dirty] (no NUM) - describe_out, rc = run_command(GITS, ["describe", "--tags", "--dirty", - "--always", "--long", - "--match", "%%s*" %% tag_prefix], - cwd=root) + describe_out, rc = runner(GITS, [ + "describe", "--tags", "--dirty", "--always", "--long", + "--match", f"{tag_prefix}[[:digit:]]*" + ], cwd=root) # --long was added in git-1.5.5 if describe_out is None: raise NotThisMethod("'git describe' failed") describe_out = describe_out.strip() - full_out, rc = run_command(GITS, ["rev-parse", "HEAD"], cwd=root) + full_out, rc = runner(GITS, ["rev-parse", "HEAD"], cwd=root) if full_out is None: raise NotThisMethod("'git rev-parse' failed") full_out = full_out.strip() - pieces = {} + pieces: Dict[str, Any] = {} pieces["long"] = full_out pieces["short"] = full_out[:7] # maybe improved later pieces["error"] = None + branch_name, rc = runner(GITS, ["rev-parse", "--abbrev-ref", "HEAD"], + cwd=root) + # --abbrev-ref was added in git-1.6.3 + if rc != 0 or branch_name is None: + raise NotThisMethod("'git rev-parse --abbrev-ref' returned error") + branch_name = branch_name.strip() + + if branch_name == "HEAD": + # If we aren't exactly on a branch, pick a branch which represents + # the current commit. If all else fails, we are on a branchless + # commit. + branches, rc = runner(GITS, ["branch", "--contains"], cwd=root) + # --contains was added in git-1.5.4 + if rc != 0 or branches is None: + raise NotThisMethod("'git branch --contains' returned error") + branches = branches.split("\n") + + # Remove the first line if we're running detached + if "(" in branches[0]: + branches.pop(0) + + # Strip off the leading "* " from the list of branches. + branches = [branch[2:] for branch in branches] + if "master" in branches: + branch_name = "master" + elif not branches: + branch_name = None + else: + # Pick the first branch that is returned. Good or bad. + branch_name = branches[0] + + pieces["branch"] = branch_name + # parse describe_out. It will be like TAG-NUM-gHEX[-dirty] or HEX[-dirty] # TAG might have hyphens. git_describe = describe_out @@ -699,7 +867,7 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): # TAG-NUM-gHEX mo = re.search(r'^(.+)-(\d+)-g([0-9a-f]+)$', git_describe) if not mo: - # unparseable. Maybe git-describe is misbehaving? + # unparsable. Maybe git-describe is misbehaving? pieces["error"] = ("unable to parse git-describe output: '%%s'" %% describe_out) return pieces @@ -724,26 +892,27 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): else: # HEX: no tags pieces["closest-tag"] = None - count_out, rc = run_command(GITS, ["rev-list", "HEAD", "--count"], - cwd=root) - pieces["distance"] = int(count_out) # total number of commits + out, rc = runner(GITS, ["rev-list", "HEAD", "--left-right"], cwd=root) + pieces["distance"] = len(out.split()) # total number of commits # commit date: see ISO-8601 comment in git_versions_from_keywords() - date = run_command(GITS, ["show", "-s", "--format=%%ci", "HEAD"], - cwd=root)[0].strip() + date = runner(GITS, ["show", "-s", "--format=%%ci", "HEAD"], cwd=root)[0].strip() + # Use only the last line. Previous lines may contain GPG signature + # information. + date = date.splitlines()[-1] pieces["date"] = date.strip().replace(" ", "T", 1).replace(" ", "", 1) return pieces -def plus_or_dot(pieces): +def plus_or_dot(pieces: Dict[str, Any]) -> str: """Return a + if we don't already have one, else return a .""" if "+" in pieces.get("closest-tag", ""): return "." return "+" -def render_pep440(pieces): +def render_pep440(pieces: Dict[str, Any]) -> str: """Build up version string, with post-release "local version identifier". Our goal: TAG[+DISTANCE.gHEX[.dirty]] . Note that if you @@ -768,23 +937,71 @@ def render_pep440(pieces): return rendered -def render_pep440_pre(pieces): - """TAG[.post.devDISTANCE] -- No -dirty. +def render_pep440_branch(pieces: Dict[str, Any]) -> str: + """TAG[[.dev0]+DISTANCE.gHEX[.dirty]] . + + The ".dev0" means not master branch. Note that .dev0 sorts backwards + (a feature branch will appear "older" than the master branch). Exceptions: - 1: no tags. 0.post.devDISTANCE + 1: no tags. 0[.dev0]+untagged.DISTANCE.gHEX[.dirty] """ if pieces["closest-tag"]: rendered = pieces["closest-tag"] + if pieces["distance"] or pieces["dirty"]: + if pieces["branch"] != "master": + rendered += ".dev0" + rendered += plus_or_dot(pieces) + rendered += "%%d.g%%s" %% (pieces["distance"], pieces["short"]) + if pieces["dirty"]: + rendered += ".dirty" + else: + # exception #1 + rendered = "0" + if pieces["branch"] != "master": + rendered += ".dev0" + rendered += "+untagged.%%d.g%%s" %% (pieces["distance"], + pieces["short"]) + if pieces["dirty"]: + rendered += ".dirty" + return rendered + + +def pep440_split_post(ver: str) -> Tuple[str, Optional[int]]: + """Split pep440 version string at the post-release segment. + + Returns the release segments before the post-release and the + post-release version number (or -1 if no post-release segment is present). + """ + vc = str.split(ver, ".post") + return vc[0], int(vc[1] or 0) if len(vc) == 2 else None + + +def render_pep440_pre(pieces: Dict[str, Any]) -> str: + """TAG[.postN.devDISTANCE] -- No -dirty. + + Exceptions: + 1: no tags. 0.post0.devDISTANCE + """ + if pieces["closest-tag"]: if pieces["distance"]: - rendered += ".post.dev%%d" %% pieces["distance"] + # update the post release segment + tag_version, post_version = pep440_split_post(pieces["closest-tag"]) + rendered = tag_version + if post_version is not None: + rendered += ".post%%d.dev%%d" %% (post_version + 1, pieces["distance"]) + else: + rendered += ".post0.dev%%d" %% (pieces["distance"]) + else: + # no commits, use the tag as the version + rendered = pieces["closest-tag"] else: # exception #1 - rendered = "0.post.dev%%d" %% pieces["distance"] + rendered = "0.post0.dev%%d" %% pieces["distance"] return rendered -def render_pep440_post(pieces): +def render_pep440_post(pieces: Dict[str, Any]) -> str: """TAG[.postDISTANCE[.dev0]+gHEX] . The ".dev0" means dirty. Note that .dev0 sorts backwards @@ -811,12 +1028,41 @@ def render_pep440_post(pieces): return rendered -def render_pep440_old(pieces): +def render_pep440_post_branch(pieces: Dict[str, Any]) -> str: + """TAG[.postDISTANCE[.dev0]+gHEX[.dirty]] . + + The ".dev0" means not master branch. + + Exceptions: + 1: no tags. 0.postDISTANCE[.dev0]+gHEX[.dirty] + """ + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + if pieces["distance"] or pieces["dirty"]: + rendered += ".post%%d" %% pieces["distance"] + if pieces["branch"] != "master": + rendered += ".dev0" + rendered += plus_or_dot(pieces) + rendered += "g%%s" %% pieces["short"] + if pieces["dirty"]: + rendered += ".dirty" + else: + # exception #1 + rendered = "0.post%%d" %% pieces["distance"] + if pieces["branch"] != "master": + rendered += ".dev0" + rendered += "+g%%s" %% pieces["short"] + if pieces["dirty"]: + rendered += ".dirty" + return rendered + + +def render_pep440_old(pieces: Dict[str, Any]) -> str: """TAG[.postDISTANCE[.dev0]] . The ".dev0" means dirty. - Eexceptions: + Exceptions: 1: no tags. 0.postDISTANCE[.dev0] """ if pieces["closest-tag"]: @@ -833,7 +1079,7 @@ def render_pep440_old(pieces): return rendered -def render_git_describe(pieces): +def render_git_describe(pieces: Dict[str, Any]) -> str: """TAG[-DISTANCE-gHEX][-dirty]. Like 'git describe --tags --dirty --always'. @@ -853,7 +1099,7 @@ def render_git_describe(pieces): return rendered -def render_git_describe_long(pieces): +def render_git_describe_long(pieces: Dict[str, Any]) -> str: """TAG-DISTANCE-gHEX[-dirty]. Like 'git describe --tags --dirty --always -long'. @@ -873,7 +1119,7 @@ def render_git_describe_long(pieces): return rendered -def render(pieces, style): +def render(pieces: Dict[str, Any], style: str) -> Dict[str, Any]: """Render the given version pieces into the requested style.""" if pieces["error"]: return {"version": "unknown", @@ -887,10 +1133,14 @@ def render(pieces, style): if style == "pep440": rendered = render_pep440(pieces) + elif style == "pep440-branch": + rendered = render_pep440_branch(pieces) elif style == "pep440-pre": rendered = render_pep440_pre(pieces) elif style == "pep440-post": rendered = render_pep440_post(pieces) + elif style == "pep440-post-branch": + rendered = render_pep440_post_branch(pieces) elif style == "pep440-old": rendered = render_pep440_old(pieces) elif style == "git-describe": @@ -905,7 +1155,7 @@ def render(pieces, style): "date": pieces.get("date")} -def get_versions(): +def get_versions() -> Dict[str, Any]: """Get version information or return default if unable to do so.""" # I am in _version.py, which lives at ROOT/VERSIONFILE_SOURCE. If we have # __file__, we can work backwards from there to the root. Some @@ -926,7 +1176,7 @@ def get_versions(): # versionfile_source is the relative path from the top of the source # tree (where the .git directory might live) to this file. Invert # this to find the root from __file__. - for i in cfg.versionfile_source.split('/'): + for _ in cfg.versionfile_source.split('/'): root = os.path.dirname(root) except NameError: return {"version": "0+unknown", "full-revisionid": None, @@ -953,41 +1203,48 @@ def get_versions(): @register_vcs_handler("git", "get_keywords") -def git_get_keywords(versionfile_abs): +def git_get_keywords(versionfile_abs: str) -> Dict[str, str]: """Extract version information from the given file.""" # the code embedded in _version.py can just fetch the value of these # keywords. When used from setup.py, we don't want to import _version.py, # so we do it with a regexp instead. This function is not used from # _version.py. - keywords = {} + keywords: Dict[str, str] = {} try: - f = open(versionfile_abs, "r") - for line in f.readlines(): - if line.strip().startswith("git_refnames ="): - mo = re.search(r'=\s*"(.*)"', line) - if mo: - keywords["refnames"] = mo.group(1) - if line.strip().startswith("git_full ="): - mo = re.search(r'=\s*"(.*)"', line) - if mo: - keywords["full"] = mo.group(1) - if line.strip().startswith("git_date ="): - mo = re.search(r'=\s*"(.*)"', line) - if mo: - keywords["date"] = mo.group(1) - f.close() - except EnvironmentError: + with open(versionfile_abs, "r") as fobj: + for line in fobj: + if line.strip().startswith("git_refnames ="): + mo = re.search(r'=\s*"(.*)"', line) + if mo: + keywords["refnames"] = mo.group(1) + if line.strip().startswith("git_full ="): + mo = re.search(r'=\s*"(.*)"', line) + if mo: + keywords["full"] = mo.group(1) + if line.strip().startswith("git_date ="): + mo = re.search(r'=\s*"(.*)"', line) + if mo: + keywords["date"] = mo.group(1) + except OSError: pass return keywords @register_vcs_handler("git", "keywords") -def git_versions_from_keywords(keywords, tag_prefix, verbose): +def git_versions_from_keywords( + keywords: Dict[str, str], + tag_prefix: str, + verbose: bool, +) -> Dict[str, Any]: """Get version information from git keywords.""" - if not keywords: - raise NotThisMethod("no keywords at all, weird") + if "refnames" not in keywords: + raise NotThisMethod("Short version file found") date = keywords.get("date") if date is not None: + # Use only the last line. Previous lines may contain GPG signature + # information. + date = date.splitlines()[-1] + # git-2.2.0 added "%cI", which expands to an ISO-8601 -compliant # datestamp. However we prefer "%ci" (which expands to an "ISO-8601 # -like" string, which we must then edit to make compliant), because @@ -1000,11 +1257,11 @@ def git_versions_from_keywords(keywords, tag_prefix, verbose): if verbose: print("keywords are unexpanded, not using") raise NotThisMethod("unexpanded keywords, not a git-archive tarball") - refs = set([r.strip() for r in refnames.strip("()").split(",")]) + refs = {r.strip() for r in refnames.strip("()").split(",")} # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of # just "foo-1.0". If we see a "tag: " prefix, prefer those. TAG = "tag: " - tags = set([r[len(TAG) :] for r in refs if r.startswith(TAG)]) + tags = {r[len(TAG) :] for r in refs if r.startswith(TAG)} if not tags: # Either we're using git < 1.8.3, or there really are no tags. We use # a heuristic: assume all version tags have a digit. The old git %d @@ -1013,7 +1270,7 @@ def git_versions_from_keywords(keywords, tag_prefix, verbose): # between branches and tags. By ignoring refnames without digits, we # filter out many common branch names like "release" and # "stabilization", as well as "HEAD" and "master". - tags = set([r for r in refs if re.search(r"\d", r)]) + tags = {r for r in refs if re.search(r"\d", r)} if verbose: print("discarding '%s', no digits" % ",".join(refs - tags)) if verbose: @@ -1022,6 +1279,11 @@ def git_versions_from_keywords(keywords, tag_prefix, verbose): # sorting will prefer e.g. "2.0" over "2.0rc1" if ref.startswith(tag_prefix): r = ref[len(tag_prefix) :] + # Filter out refs that exactly match prefix or that don't start + # with a number once the prefix is stripped (mostly a concern + # when prefix is '') + if not re.match(r"\d", r): + continue if verbose: print("picking %s" % r) return { @@ -1044,7 +1306,9 @@ def git_versions_from_keywords(keywords, tag_prefix, verbose): @register_vcs_handler("git", "pieces_from_vcs") -def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): +def git_pieces_from_vcs( + tag_prefix: str, root: str, verbose: bool, runner: Callable = run_command +) -> Dict[str, Any]: """Get version from 'git describe' in the root of the source tree. This only gets called if the git-archive 'subst' keywords were *not* @@ -1055,7 +1319,14 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): if sys.platform == "win32": GITS = ["git.cmd", "git.exe"] - out, rc = run_command(GITS, ["rev-parse", "--git-dir"], cwd=root, hide_stderr=True) + # GIT_DIR can interfere with correct operation of Versioneer. + # It may be intended to be passed to the Versioneer-versioned project, + # but that should not change where we get our version from. + env = os.environ.copy() + env.pop("GIT_DIR", None) + runner = functools.partial(runner, env=env) + + _, rc = runner(GITS, ["rev-parse", "--git-dir"], cwd=root, hide_stderr=not verbose) if rc != 0: if verbose: print("Directory %s not under git control" % root) @@ -1063,7 +1334,7 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): # if there is a tag matching tag_prefix, this yields TAG-NUM-gHEX[-dirty] # if there isn't one, this yields HEX[-dirty] (no NUM) - describe_out, rc = run_command( + describe_out, rc = runner( GITS, [ "describe", @@ -1072,7 +1343,7 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): "--always", "--long", "--match", - "%s*" % tag_prefix, + f"{tag_prefix}[[:digit:]]*", ], cwd=root, ) @@ -1080,16 +1351,48 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): if describe_out is None: raise NotThisMethod("'git describe' failed") describe_out = describe_out.strip() - full_out, rc = run_command(GITS, ["rev-parse", "HEAD"], cwd=root) + full_out, rc = runner(GITS, ["rev-parse", "HEAD"], cwd=root) if full_out is None: raise NotThisMethod("'git rev-parse' failed") full_out = full_out.strip() - pieces = {} + pieces: Dict[str, Any] = {} pieces["long"] = full_out pieces["short"] = full_out[:7] # maybe improved later pieces["error"] = None + branch_name, rc = runner(GITS, ["rev-parse", "--abbrev-ref", "HEAD"], cwd=root) + # --abbrev-ref was added in git-1.6.3 + if rc != 0 or branch_name is None: + raise NotThisMethod("'git rev-parse --abbrev-ref' returned error") + branch_name = branch_name.strip() + + if branch_name == "HEAD": + # If we aren't exactly on a branch, pick a branch which represents + # the current commit. If all else fails, we are on a branchless + # commit. + branches, rc = runner(GITS, ["branch", "--contains"], cwd=root) + # --contains was added in git-1.5.4 + if rc != 0 or branches is None: + raise NotThisMethod("'git branch --contains' returned error") + branches = branches.split("\n") + + # Remove the first line if we're running detached + if "(" in branches[0]: + branches.pop(0) + + # Strip off the leading "* " from the list of branches. + branches = [branch[2:] for branch in branches] + if "master" in branches: + branch_name = "master" + elif not branches: + branch_name = None + else: + # Pick the first branch that is returned. Good or bad. + branch_name = branches[0] + + pieces["branch"] = branch_name + # parse describe_out. It will be like TAG-NUM-gHEX[-dirty] or HEX[-dirty] # TAG might have hyphens. git_describe = describe_out @@ -1106,7 +1409,7 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): # TAG-NUM-gHEX mo = re.search(r"^(.+)-(\d+)-g([0-9a-f]+)$", git_describe) if not mo: - # unparseable. Maybe git-describe is misbehaving? + # unparsable. Maybe git-describe is misbehaving? pieces["error"] = "unable to parse git-describe output: '%s'" % describe_out return pieces @@ -1132,19 +1435,20 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): else: # HEX: no tags pieces["closest-tag"] = None - count_out, rc = run_command(GITS, ["rev-list", "HEAD", "--count"], cwd=root) - pieces["distance"] = int(count_out) # total number of commits + out, rc = runner(GITS, ["rev-list", "HEAD", "--left-right"], cwd=root) + pieces["distance"] = len(out.split()) # total number of commits # commit date: see ISO-8601 comment in git_versions_from_keywords() - date = run_command(GITS, ["show", "-s", "--format=%ci", "HEAD"], cwd=root)[ - 0 - ].strip() + date = runner(GITS, ["show", "-s", "--format=%ci", "HEAD"], cwd=root)[0].strip() + # Use only the last line. Previous lines may contain GPG signature + # information. + date = date.splitlines()[-1] pieces["date"] = date.strip().replace(" ", "T", 1).replace(" ", "", 1) return pieces -def do_vcs_install(manifest_in, versionfile_source, ipy): +def do_vcs_install(versionfile_source: str, ipy: Optional[str]) -> None: """Git-specific installation logic for Versioneer. For Git, this means creating/changing .gitattributes to mark _version.py @@ -1153,36 +1457,40 @@ def do_vcs_install(manifest_in, versionfile_source, ipy): GITS = ["git"] if sys.platform == "win32": GITS = ["git.cmd", "git.exe"] - files = [manifest_in, versionfile_source] + files = [versionfile_source] if ipy: files.append(ipy) - try: - me = __file__ - if me.endswith(".pyc") or me.endswith(".pyo"): - me = os.path.splitext(me)[0] + ".py" - versioneer_file = os.path.relpath(me) - except NameError: - versioneer_file = "versioneer.py" - files.append(versioneer_file) + if "VERSIONEER_PEP518" not in globals(): + try: + my_path = __file__ + if my_path.endswith((".pyc", ".pyo")): + my_path = os.path.splitext(my_path)[0] + ".py" + versioneer_file = os.path.relpath(my_path) + except NameError: + versioneer_file = "versioneer.py" + files.append(versioneer_file) present = False try: - f = open(".gitattributes", "r") - for line in f.readlines(): - if line.strip().startswith(versionfile_source): - if "export-subst" in line.strip().split()[1:]: - present = True - f.close() - except EnvironmentError: + with open(".gitattributes", "r") as fobj: + for line in fobj: + if line.strip().startswith(versionfile_source): + if "export-subst" in line.strip().split()[1:]: + present = True + break + except OSError: pass if not present: - f = open(".gitattributes", "a+") - f.write("%s export-subst\n" % versionfile_source) - f.close() + with open(".gitattributes", "a+") as fobj: + fobj.write(f"{versionfile_source} export-subst\n") files.append(".gitattributes") run_command(GITS, ["add", "--"] + files) -def versions_from_parentdir(parentdir_prefix, root, verbose): +def versions_from_parentdir( + parentdir_prefix: str, + root: str, + verbose: bool, +) -> Dict[str, Any]: """Try to determine the version from the parent directory name. Source tarballs conventionally unpack into a directory that includes both @@ -1191,7 +1499,7 @@ def versions_from_parentdir(parentdir_prefix, root, verbose): """ rootdirs = [] - for i in range(3): + for _ in range(3): dirname = os.path.basename(root) if dirname.startswith(parentdir_prefix): return { @@ -1201,9 +1509,8 @@ def versions_from_parentdir(parentdir_prefix, root, verbose): "error": None, "date": None, } - else: - rootdirs.append(root) - root = os.path.dirname(root) # up a level + rootdirs.append(root) + root = os.path.dirname(root) # up a level if verbose: print( @@ -1214,7 +1521,7 @@ def versions_from_parentdir(parentdir_prefix, root, verbose): SHORT_VERSION_PY = """ -# This file was generated by 'versioneer.py' (0.18) from +# This file was generated by 'versioneer.py' (0.29) from # revision-control system data, or from the parent directory name of an # unpacked source archive. Distribution tarballs contain a pre-generated copy # of this file. @@ -1231,12 +1538,12 @@ def get_versions(): """ -def versions_from_file(filename): +def versions_from_file(filename: str) -> Dict[str, Any]: """Try to determine the version from _version.py if present.""" try: with open(filename) as f: contents = f.read() - except EnvironmentError: + except OSError: raise NotThisMethod("unable to read _version.py") mo = re.search( r"version_json = '''\n(.*)''' # END VERSION_JSON", contents, re.M | re.S @@ -1250,9 +1557,8 @@ def versions_from_file(filename): return json.loads(mo.group(1)) -def write_to_version_file(filename, versions): +def write_to_version_file(filename: str, versions: Dict[str, Any]) -> None: """Write the given version number to the given _version.py file.""" - os.unlink(filename) contents = json.dumps(versions, sort_keys=True, indent=1, separators=(",", ": ")) with open(filename, "w") as f: f.write(SHORT_VERSION_PY % contents) @@ -1260,14 +1566,14 @@ def write_to_version_file(filename, versions): print("set %s to '%s'" % (filename, versions["version"])) -def plus_or_dot(pieces): +def plus_or_dot(pieces: Dict[str, Any]) -> str: """Return a + if we don't already have one, else return a .""" if "+" in pieces.get("closest-tag", ""): return "." return "+" -def render_pep440(pieces): +def render_pep440(pieces: Dict[str, Any]) -> str: """Build up version string, with post-release "local version identifier". Our goal: TAG[+DISTANCE.gHEX[.dirty]] . Note that if you @@ -1291,23 +1597,70 @@ def render_pep440(pieces): return rendered -def render_pep440_pre(pieces): - """TAG[.post.devDISTANCE] -- No -dirty. +def render_pep440_branch(pieces: Dict[str, Any]) -> str: + """TAG[[.dev0]+DISTANCE.gHEX[.dirty]] . + + The ".dev0" means not master branch. Note that .dev0 sorts backwards + (a feature branch will appear "older" than the master branch). Exceptions: - 1: no tags. 0.post.devDISTANCE + 1: no tags. 0[.dev0]+untagged.DISTANCE.gHEX[.dirty] """ if pieces["closest-tag"]: rendered = pieces["closest-tag"] + if pieces["distance"] or pieces["dirty"]: + if pieces["branch"] != "master": + rendered += ".dev0" + rendered += plus_or_dot(pieces) + rendered += "%d.g%s" % (pieces["distance"], pieces["short"]) + if pieces["dirty"]: + rendered += ".dirty" + else: + # exception #1 + rendered = "0" + if pieces["branch"] != "master": + rendered += ".dev0" + rendered += "+untagged.%d.g%s" % (pieces["distance"], pieces["short"]) + if pieces["dirty"]: + rendered += ".dirty" + return rendered + + +def pep440_split_post(ver: str) -> Tuple[str, Optional[int]]: + """Split pep440 version string at the post-release segment. + + Returns the release segments before the post-release and the + post-release version number (or -1 if no post-release segment is present). + """ + vc = str.split(ver, ".post") + return vc[0], int(vc[1] or 0) if len(vc) == 2 else None + + +def render_pep440_pre(pieces: Dict[str, Any]) -> str: + """TAG[.postN.devDISTANCE] -- No -dirty. + + Exceptions: + 1: no tags. 0.post0.devDISTANCE + """ + if pieces["closest-tag"]: if pieces["distance"]: - rendered += ".post.dev%d" % pieces["distance"] + # update the post release segment + tag_version, post_version = pep440_split_post(pieces["closest-tag"]) + rendered = tag_version + if post_version is not None: + rendered += ".post%d.dev%d" % (post_version + 1, pieces["distance"]) + else: + rendered += ".post0.dev%d" % (pieces["distance"]) + else: + # no commits, use the tag as the version + rendered = pieces["closest-tag"] else: # exception #1 - rendered = "0.post.dev%d" % pieces["distance"] + rendered = "0.post0.dev%d" % pieces["distance"] return rendered -def render_pep440_post(pieces): +def render_pep440_post(pieces: Dict[str, Any]) -> str: """TAG[.postDISTANCE[.dev0]+gHEX] . The ".dev0" means dirty. Note that .dev0 sorts backwards @@ -1334,12 +1687,41 @@ def render_pep440_post(pieces): return rendered -def render_pep440_old(pieces): +def render_pep440_post_branch(pieces: Dict[str, Any]) -> str: + """TAG[.postDISTANCE[.dev0]+gHEX[.dirty]] . + + The ".dev0" means not master branch. + + Exceptions: + 1: no tags. 0.postDISTANCE[.dev0]+gHEX[.dirty] + """ + if pieces["closest-tag"]: + rendered = pieces["closest-tag"] + if pieces["distance"] or pieces["dirty"]: + rendered += ".post%d" % pieces["distance"] + if pieces["branch"] != "master": + rendered += ".dev0" + rendered += plus_or_dot(pieces) + rendered += "g%s" % pieces["short"] + if pieces["dirty"]: + rendered += ".dirty" + else: + # exception #1 + rendered = "0.post%d" % pieces["distance"] + if pieces["branch"] != "master": + rendered += ".dev0" + rendered += "+g%s" % pieces["short"] + if pieces["dirty"]: + rendered += ".dirty" + return rendered + + +def render_pep440_old(pieces: Dict[str, Any]) -> str: """TAG[.postDISTANCE[.dev0]] . The ".dev0" means dirty. - Eexceptions: + Exceptions: 1: no tags. 0.postDISTANCE[.dev0] """ if pieces["closest-tag"]: @@ -1356,7 +1738,7 @@ def render_pep440_old(pieces): return rendered -def render_git_describe(pieces): +def render_git_describe(pieces: Dict[str, Any]) -> str: """TAG[-DISTANCE-gHEX][-dirty]. Like 'git describe --tags --dirty --always'. @@ -1376,7 +1758,7 @@ def render_git_describe(pieces): return rendered -def render_git_describe_long(pieces): +def render_git_describe_long(pieces: Dict[str, Any]) -> str: """TAG-DISTANCE-gHEX[-dirty]. Like 'git describe --tags --dirty --always -long'. @@ -1396,7 +1778,7 @@ def render_git_describe_long(pieces): return rendered -def render(pieces, style): +def render(pieces: Dict[str, Any], style: str) -> Dict[str, Any]: """Render the given version pieces into the requested style.""" if pieces["error"]: return { @@ -1412,10 +1794,14 @@ def render(pieces, style): if style == "pep440": rendered = render_pep440(pieces) + elif style == "pep440-branch": + rendered = render_pep440_branch(pieces) elif style == "pep440-pre": rendered = render_pep440_pre(pieces) elif style == "pep440-post": rendered = render_pep440_post(pieces) + elif style == "pep440-post-branch": + rendered = render_pep440_post_branch(pieces) elif style == "pep440-old": rendered = render_pep440_old(pieces) elif style == "git-describe": @@ -1438,7 +1824,7 @@ class VersioneerBadRootError(Exception): """The project root directory is unknown or missing key files.""" -def get_versions(verbose=False): +def get_versions(verbose: bool = False) -> Dict[str, Any]: """Get the project version from whatever source is available. Returns dict with two keys: 'version' and 'full'. @@ -1453,7 +1839,7 @@ def get_versions(verbose=False): assert cfg.VCS is not None, "please set [versioneer]VCS= in setup.cfg" handlers = HANDLERS.get(cfg.VCS) assert handlers, "unrecognized VCS '%s'" % cfg.VCS - verbose = verbose or cfg.verbose + verbose = verbose or bool(cfg.verbose) # `bool()` used to avoid `None` assert ( cfg.versionfile_source is not None ), "please set versioneer.versionfile_source" @@ -1519,13 +1905,17 @@ def get_versions(verbose=False): } -def get_version(): +def get_version() -> str: """Get the short version string for this project.""" return get_versions()["version"] -def get_cmdclass(): - """Get the custom setuptools/distutils subclasses used by Versioneer.""" +def get_cmdclass(cmdclass: Optional[Dict[str, Any]] = None): + """Get the custom setuptools subclasses used by Versioneer. + + If the package uses a different cmdclass (e.g. one from numpy), it + should be provide as an argument. + """ if "versioneer" in sys.modules: del sys.modules["versioneer"] # this fixes the "python setup.py develop" case (also 'install' and @@ -1539,25 +1929,25 @@ def get_cmdclass(): # parent is protected against the child's "import versioneer". By # removing ourselves from sys.modules here, before the child build # happens, we protect the child from the parent's versioneer too. - # Also see https://github.com/warner/python-versioneer/issues/52 + # Also see https://github.com/python-versioneer/python-versioneer/issues/52 - cmds = {} + cmds = {} if cmdclass is None else cmdclass.copy() - # we add "version" to both distutils and setuptools - from distutils.core import Command + # we add "version" to setuptools + from setuptools import Command class cmd_version(Command): description = "report generated version string" - user_options = [] - boolean_options = [] + user_options: List[Tuple[str, str, str]] = [] + boolean_options: List[str] = [] - def initialize_options(self): + def initialize_options(self) -> None: pass - def finalize_options(self): + def finalize_options(self) -> None: pass - def run(self): + def run(self) -> None: vers = get_versions(verbose=True) print("Version: %s" % vers["version"]) print(" full-revisionid: %s" % vers.get("full-revisionid")) @@ -1568,7 +1958,7 @@ def run(self): cmds["version"] = cmd_version - # we override "build_py" in both distutils and setuptools + # we override "build_py" in setuptools # # most invocation pathways end up running build_py: # distutils/build -> build_py @@ -1583,18 +1973,25 @@ def run(self): # then does setup.py bdist_wheel, or sometimes setup.py install # setup.py egg_info -> ? + # pip install -e . and setuptool/editable_wheel will invoke build_py + # but the build_py command is not expected to copy any files. + # we override different "build_py" commands for both environments - if "setuptools" in sys.modules: - from setuptools.command.build_py import build_py as _build_py + if "build_py" in cmds: + _build_py: Any = cmds["build_py"] else: - from distutils.command.build_py import build_py as _build_py + from setuptools.command.build_py import build_py as _build_py class cmd_build_py(_build_py): - def run(self): + def run(self) -> None: root = get_root() cfg = get_config_from_root(root) versions = get_versions() _build_py.run(self) + if getattr(self, "editable_mode", False): + # During editable installs `.py` and data files are + # not copied to build_lib + return # now locate _version.py in the new build/ directory and replace # it with an updated value if cfg.versionfile_build: @@ -1604,8 +2001,42 @@ def run(self): cmds["build_py"] = cmd_build_py + if "build_ext" in cmds: + _build_ext: Any = cmds["build_ext"] + else: + from setuptools.command.build_ext import build_ext as _build_ext + + class cmd_build_ext(_build_ext): + def run(self) -> None: + root = get_root() + cfg = get_config_from_root(root) + versions = get_versions() + _build_ext.run(self) + if self.inplace: + # build_ext --inplace will only build extensions in + # build/lib<..> dir with no _version.py to write to. + # As in place builds will already have a _version.py + # in the module dir, we do not need to write one. + return + # now locate _version.py in the new build/ directory and replace + # it with an updated value + if not cfg.versionfile_build: + return + target_versionfile = os.path.join(self.build_lib, cfg.versionfile_build) + if not os.path.exists(target_versionfile): + print( + f"Warning: {target_versionfile} does not exist, skipping " + "version update. This can happen if you are running build_ext " + "without first running build_py." + ) + return + print("UPDATING %s" % target_versionfile) + write_to_version_file(target_versionfile, versions) + + cmds["build_ext"] = cmd_build_ext + if "cx_Freeze" in sys.modules: # cx_freeze enabled? - from cx_Freeze.dist import build_exe as _build_exe + from cx_Freeze.dist import build_exe as _build_exe # type: ignore # nczeczulin reports that py2exe won't like the pep440-style string # as FILEVERSION, but it can be used for PRODUCTVERSION, e.g. @@ -1615,7 +2046,7 @@ def run(self): # ... class cmd_build_exe(_build_exe): - def run(self): + def run(self) -> None: root = get_root() cfg = get_config_from_root(root) versions = get_versions() @@ -1643,12 +2074,12 @@ def run(self): if "py2exe" in sys.modules: # py2exe enabled? try: - from py2exe.distutils_buildexe import py2exe as _py2exe # py3 + from py2exe.setuptools_buildexe import py2exe as _py2exe # type: ignore except ImportError: - from py2exe.build_exe import py2exe as _py2exe # py2 + from py2exe.distutils_buildexe import py2exe as _py2exe # type: ignore class cmd_py2exe(_py2exe): - def run(self): + def run(self) -> None: root = get_root() cfg = get_config_from_root(root) versions = get_versions() @@ -1673,14 +2104,54 @@ def run(self): cmds["py2exe"] = cmd_py2exe + # sdist farms its file list building out to egg_info + if "egg_info" in cmds: + _egg_info: Any = cmds["egg_info"] + else: + from setuptools.command.egg_info import egg_info as _egg_info + + class cmd_egg_info(_egg_info): + def find_sources(self) -> None: + # egg_info.find_sources builds the manifest list and writes it + # in one shot + super().find_sources() + + # Modify the filelist and normalize it + root = get_root() + cfg = get_config_from_root(root) + self.filelist.append("versioneer.py") + if cfg.versionfile_source: + # There are rare cases where versionfile_source might not be + # included by default, so we must be explicit + self.filelist.append(cfg.versionfile_source) + self.filelist.sort() + self.filelist.remove_duplicates() + + # The write method is hidden in the manifest_maker instance that + # generated the filelist and was thrown away + # We will instead replicate their final normalization (to unicode, + # and POSIX-style paths) + from setuptools import unicode_utils + + normalized = [ + unicode_utils.filesys_decode(f).replace(os.sep, "/") + for f in self.filelist.files + ] + + manifest_filename = os.path.join(self.egg_info, "SOURCES.txt") + with open(manifest_filename, "w") as fobj: + fobj.write("\n".join(normalized)) + + cmds["egg_info"] = cmd_egg_info + # we override different "sdist" commands for both environments - if "setuptools" in sys.modules: - from setuptools.command.sdist import sdist as _sdist + if "sdist" in cmds: + _sdist: Any = cmds["sdist"] else: - from distutils.command.sdist import sdist as _sdist + from setuptools.command.sdist import sdist as _sdist class cmd_sdist(_sdist): - def run(self): + def run(self) -> None: versions = get_versions() self._versioneer_generated_versions = versions # unless we update this, the command will keep using the old @@ -1688,7 +2159,7 @@ def run(self): self.distribution.metadata.version = versions["version"] return _sdist.run(self) - def make_release_tree(self, base_dir, files): + def make_release_tree(self, base_dir: str, files: List[str]) -> None: root = get_root() cfg = get_config_from_root(root) _sdist.make_release_tree(self, base_dir, files) @@ -1743,24 +2214,25 @@ def make_release_tree(self, base_dir, files): """ -INIT_PY_SNIPPET = """ +OLD_SNIPPET = """ from ._version import get_versions __version__ = get_versions()['version'] del get_versions """ +INIT_PY_SNIPPET = """ +from . import {0} +__version__ = {0}.get_versions()['version'] +""" + -def do_setup(): - """Main VCS-independent setup function for installing Versioneer.""" +def do_setup() -> int: + """Do main VCS-independent setup function for installing Versioneer.""" root = get_root() try: cfg = get_config_from_root(root) - except ( - EnvironmentError, - configparser.NoSectionError, - configparser.NoOptionError, - ) as e: - if isinstance(e, (EnvironmentError, configparser.NoSectionError)): + except (OSError, configparser.NoSectionError, configparser.NoOptionError) as e: + if isinstance(e, (OSError, configparser.NoSectionError)): print("Adding sample versioneer config to setup.cfg", file=sys.stderr) with open(os.path.join(root, "setup.cfg"), "a") as f: f.write(SAMPLE_CONFIG) @@ -1782,64 +2254,37 @@ def do_setup(): ) ipy = os.path.join(os.path.dirname(cfg.versionfile_source), "__init__.py") + maybe_ipy: Optional[str] = ipy if os.path.exists(ipy): try: with open(ipy, "r") as f: old = f.read() - except EnvironmentError: + except OSError: old = "" - if INIT_PY_SNIPPET not in old: + module = os.path.splitext(os.path.basename(cfg.versionfile_source))[0] + snippet = INIT_PY_SNIPPET.format(module) + if OLD_SNIPPET in old: + print(" replacing boilerplate in %s" % ipy) + with open(ipy, "w") as f: + f.write(old.replace(OLD_SNIPPET, snippet)) + elif snippet not in old: print(" appending to %s" % ipy) with open(ipy, "a") as f: - f.write(INIT_PY_SNIPPET) + f.write(snippet) else: print(" %s unmodified" % ipy) else: print(" %s doesn't exist, ok" % ipy) - ipy = None - - # Make sure both the top-level "versioneer.py" and versionfile_source - # (PKG/_version.py, used by runtime code) are in MANIFEST.in, so - # they'll be copied into source distributions. Pip won't be able to - # install the package without this. - manifest_in = os.path.join(root, "MANIFEST.in") - simple_includes = set() - try: - with open(manifest_in, "r") as f: - for line in f: - if line.startswith("include "): - for include in line.split()[1:]: - simple_includes.add(include) - except EnvironmentError: - pass - # That doesn't cover everything MANIFEST.in can do - # (http://docs.python.org/2/distutils/sourcedist.html#commands), so - # it might give some false negatives. Appending redundant 'include' - # lines is safe, though. - if "versioneer.py" not in simple_includes: - print(" appending 'versioneer.py' to MANIFEST.in") - with open(manifest_in, "a") as f: - f.write("include versioneer.py\n") - else: - print(" 'versioneer.py' already in MANIFEST.in") - if cfg.versionfile_source not in simple_includes: - print( - " appending versionfile_source ('%s') to MANIFEST.in" - % cfg.versionfile_source - ) - with open(manifest_in, "a") as f: - f.write("include %s\n" % cfg.versionfile_source) - else: - print(" versionfile_source already in MANIFEST.in") + maybe_ipy = None # Make VCS-specific changes. For git, this means creating/changing # .gitattributes to mark _version.py for export-subst keyword # substitution. - do_vcs_install(manifest_in, cfg.versionfile_source, ipy) + do_vcs_install(cfg.versionfile_source, maybe_ipy) return 0 -def scan_setup_py(): +def scan_setup_py() -> int: """Validate the contents of setup.py against Versioneer's expectations.""" found = set() setters = False @@ -1876,10 +2321,14 @@ def scan_setup_py(): return errors +def setup_command() -> NoReturn: + """Set up Versioneer and exit with appropriate error code.""" + errors = do_setup() + errors += scan_setup_py() + sys.exit(1 if errors else 0) + + if __name__ == "__main__": cmd = sys.argv[1] if cmd == "setup": - errors = do_setup() - errors += scan_setup_py() - if errors: - sys.exit(1) + setup_command()