Skip to content
Snippets Groups Projects
Commit ae3fd182 authored by robbinespu's avatar robbinespu
Browse files

New upstream version 0.3.1

parents
No related branches found
No related tags found
No related merge requests found
sinfo/_version.py export-subst
# single files/folder are searched for in any directory
# paths are only from the top directory
*.pyc
build/
dist/
.cache/
.pytest_cache/
.coverage
sinfo.egg-info/
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## [0.3.1] - 20200115
### Changed
- HTML defaults to False for output to show up on GitHub.
## [0.3.0] - 2019-12-14
### Added
- Support for tuples version attributes.
- Print sinfo version by default.
- Output as concealed HTML for notebooks.
- Detect notebook name and prefix requirements file accordingly.
- Docstring for requirements text file parameters.
- Detect desirable behavior when parameters are set to `None`.
- For html, jupyter, and requirements file.
### Changed
- Parameters are no longer prefaced with `print_`.
### Fixed
- Remove duplicate IPython output.
This is a compilation of what I consider the key points from the
[Mozilla](https://www.mozilla.org/en-US/about/governance/policies/participation/),
[Jupyter](https://github.com/jupyter/governance/blob/master/conduct/code_of_conduct.md),
and [Ubuntu](https://www.ubuntu.com/community/code-of-conduct) community
guidelines. This isn't an exhaustive list of things that you can't do. Rather,
it aims to provide general guidelines for successfully conducting collaborative
work in an exciting and fun environment. Please report unacceptable behavior
by [opening a confidential
issue](https://git.gutta.com/help/user/project/issues/confidential_issues.md).
- **Be welcoming, friendly, and patient.**
- We strive to be a community that welcomes and supports people of all
backgrounds and identities. Remember that you can interact with people
from all over the world and that you may be communicating with someone
with a different primary language or cultural background.
- **Be direct and respectful.**
- We must be able to speak directly when we disagree and when we think we
need to improve. We cannot withhold hard truths. Doing so respectfully
is hard, doing so when others don’t seem to be listening is harder, and
hearing such comments when one is the recipient can be even harder
still. We need to be honest and direct, as well as respectful
- **Understand and learn from disagreement and different perspectives.**
- Our goal should not be to personally “win” every disagreement or
argument. Value discussion and be open to ideas that make our own ideas
better, while also making sure to speak up when and we disagree with an
idea and explain why. Being unable to understand why someone holds
a viewpoint doesn’t mean that they’re wrong. Don’t forget that it is
human to err and blaming each other doesn’t get us anywhere, rather offer
to help resolving issues and to help learn from mistakes. Have
a dialectic, not a debate. Actual winning is when different perspectives
make our work richer and stronger.
- **Lead by example.**
- By matching your actions with your words, you become a person others want
to follow. Your actions influence others to behave and respond in ways
that are valuable and appropriate for our organizational outcomes. Design
your community and your work for inclusion. Hold yourself and others
accountable for inclusive behaviors.
- **Ask for help when unsure.**
- Nobody is expected to be perfect. Asking questions early avoids many
problems later, so questions are encouraged, though they may be directed
to the appropriate forum. Those who are asked should be responsive and
helpful.
- **Give people the benefit of the doubt.**
- Ask for clarification instead of jumping to conclusions.
- **A simple apology can go a long way.**
- It can often de-escalate a situation, and telling someone that you are
sorry is an act of empathy that doesn’t automatically imply an admission
of guilt.
- **Be considerate in the words that you choose.**
- Do not insult or put down other community members. Harassment and other
exclusionary behavior are not acceptable. This includes, but is not
limited to
- Violent threats or violent language directed against another person
- Discriminatory jokes and language
- Posting sexually explicit or violent material
- Posting (or threatening to post) other people's personally identifying
information ("doxing")
- Personal insults, especially those using racist or sexist terms
- Unwelcome sexual attention
- Advocating for, or encouraging, any of the above behavior
- Repeated harassment of others. In general, if someone asks you to
stop, then stop.
This code of conduct is released under [CC BY-SA
3.0](https://creativecommons.org/licenses/by-sa/3.0/).
Thanks for your interest in helping to improve `sinfo`!
The best way to get in touch to discuss ideas is to [open a new
issue](https://gitlab.com/joelostblom/sinfo/issues).
Remember to follow the [Code of
conduct](https://gitlab.com/joelostblom/sinfo/blob/master/CODE_OF_CONDUCT.md)
when you participate in this project =)
LICENSE 0 → 100644
BSD 3-Clause License
Copyright (c) 2019, Joel Ostblom
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
include versioneer.py
include sinfo/_version.py
# sinfo
`sinfo` outputs version information for modules loaded in the current session,
Python, the OS, and the CPU. It is designed as a minimum measure to increase
reproducibility and provides similar information as `sessionInfo` in R. The
name is shortened to encourage regular usage through reduced typing =)
## Motivation
`sinfo` is particularly useful when conducting exploratory data analysis in
Jupyter notebooks. Listing the version numbers of all loaded modules after
importing them is a simple way to ensure a minimum level of reproducibility
while requiring little additional effort. This practice is useful both when
revisiting notebooks and when sharing them with colleagues. `sinfo` is meant to
complement more robust practices such as frozen virtual environments,
containers, and binder.
## Installation
`sinfo` can be installed via `pip install sinfo`. It does not depend on a package
manager to find version numbers since it fetches them from the module's version
string. Its only dependency is `stdlib_list`, which is used to distinguish
between standard library and third party modules.
## Usage
```python
import math
import natsort
import numpy
import pandas
from sinfo import sinfo
sinfo()
```
Output:
```
-----
natsort 5.3.3
numpy 1.17.3
pandas 0.25.1
sinfo 0.3.0
-----
Python 3.7.3 | packaged by conda-forge | (default, Dec 6 2019, 08:54:18) [GCC 7.3.0]
Linux-5.4.2-arch1-1-x86_64-with-arch
4 logical CPU cores
-----
Session information updated at 2019-12-14 16:14
```
The default behavior is to only output modules not in the standard library,
which is why the `math` module is omitted above (it can be included by
specifying `std_lib=True`). To include not only the explicitly imported
modules, but also any dependencies they import internally, specify `dependencies=True`.
The notebook output is concealed in `<details>` tags by default to not take up too much visual real estate.
When called from a notebook, `sinfo` writes the module dependencies
to a file called to `<notebook_name>-requirements.txt`, which is compatible with `pip install -r /path/to/requirements.txt`.
See the docstring for complete parameter info.
## Background
`sinfo` started as minor modifications of `py_session`, and as it grew it
became convenient to create a new package. `sinfo` was built with the help of
information provided in stackoverflow answers and existing similar packages,
including
- https://github.com/fbrundu/py_session
- https://github.com/jrjohansson/version_information
- https://stackoverflow.com/a/4858123/2166823
- https://stackoverflow.com/a/40690954/2166823
- https://stackoverflow.com/a/52187331/2166823
[versioneer]
VCS = git
style = pep440
versionfile_source = sinfo/_version.py
versionfile_build = sinfo/_version.py
tag_prefix =
parentdir_prefix =
setup.py 0 → 100644
from os import path
from setuptools import setup, find_packages
import versioneer
# Read the contents of the README file
this_directory = path.abspath(path.dirname(__file__))
with open(path.join(this_directory, 'README.md'), encoding='utf-8') as f:
long_description = f.read()
setup(
name='sinfo',
url='https://gitlab.com/joelostblom/sinfo',
author='Joel Ostblom',
author_email='joel.ostblom@protonmail.com',
packages=find_packages(),
install_requires=['stdlib_list'],
python_requires='>=3.6',
# Automatic version number
version=versioneer.get_version(),
cmdclass=versioneer.get_cmdclass(),
license='BSD-3',
description='''
sinfo outputs version information for modules loaded in the current
session, Python, and the OS.''',
# Include readme in markdown format, GFM markdown style by default
long_description=open('README.md').read(),
long_description_content_type='text/markdown'
)
from .main import sinfo
from ._version import get_versions
__version__ = get_versions()['version']
del get_versions
# 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)
"""Git implementation of _version.py."""
import errno
import os
import re
import subprocess
import sys
def get_keywords():
"""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
# each be defined on a line of their own. _version.py will just call
# get_keywords().
git_refnames = " (HEAD -> master, tag: 0.3.1, refs/keep-around/d7037becde4d587545c800b80ced85015962b588)"
git_full = "d7037becde4d587545c800b80ced85015962b588"
git_date = "2020-01-15 19:51:02 +0100"
keywords = {"refnames": git_refnames, "full": git_full, "date": git_date}
return keywords
class VersioneerConfig:
"""Container for Versioneer configuration parameters."""
def get_config():
"""Create, populate and return the VersioneerConfig() object."""
# these strings are filled in when 'setup.py versioneer' creates
# _version.py
cfg = VersioneerConfig()
cfg.VCS = "git"
cfg.style = "pep440"
cfg.tag_prefix = ""
cfg.parentdir_prefix = ""
cfg.versionfile_source = "sinfo/_version.py"
cfg.verbose = False
return cfg
class NotThisMethod(Exception):
"""Exception raised if a method is not valid for the current scenario."""
LONG_VERSION_PY = {}
HANDLERS = {}
def register_vcs_handler(vcs, method): # decorator
"""Decorator to mark a method as the handler for a particular VCS."""
def decorate(f):
"""Store f in HANDLERS[vcs][method]."""
if vcs not in HANDLERS:
HANDLERS[vcs] = {}
HANDLERS[vcs][method] = f
return f
return decorate
def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False,
env=None):
"""Call the given command(s)."""
assert isinstance(commands, list)
p = None
for c in commands:
try:
dispcmd = str([c] + 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))
break
except EnvironmentError:
e = sys.exc_info()[1]
if e.errno == errno.ENOENT:
continue
if verbose:
print("unable to run %s" % dispcmd)
print(e)
return None, None
else:
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:
if verbose:
print("unable to run %s (error)" % dispcmd)
print("stdout was %s" % stdout)
return None, p.returncode
return stdout, p.returncode
def versions_from_parentdir(parentdir_prefix, root, verbose):
"""Try to determine the version from the parent directory name.
Source tarballs conventionally unpack into a directory that includes both
the project name and a version string. We will also support searching up
two directory levels for an appropriately named parent directory
"""
rootdirs = []
for i 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
if verbose:
print("Tried directories %s but none started with prefix %s" %
(str(rootdirs), parentdir_prefix))
raise NotThisMethod("rootdir doesn't start with parentdir_prefix")
@register_vcs_handler("git", "get_keywords")
def git_get_keywords(versionfile_abs):
"""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 = {}
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:
pass
return keywords
@register_vcs_handler("git", "keywords")
def git_versions_from_keywords(keywords, tag_prefix, verbose):
"""Get version information from git keywords."""
if not keywords:
raise NotThisMethod("no keywords at all, weird")
date = keywords.get("date")
if date is not None:
# 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
# it's been around since git-1.5.3, and it's too difficult to
# discover which version we're using, or to work around using an
# older one.
date = date.strip().replace(" ", "T", 1).replace(" ", "", 1)
refnames = keywords["refnames"].strip()
if refnames.startswith("$Format"):
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(",")])
# 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)])
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
# expansion behaves like git log --decorate=short and strips out the
# refs/heads/ and refs/tags/ prefixes that would let us distinguish
# 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)])
if verbose:
print("discarding '%s', no digits" % ",".join(refs - tags))
if verbose:
print("likely tags: %s" % ",".join(sorted(tags)))
for ref in sorted(tags):
# sorting will prefer e.g. "2.0" over "2.0rc1"
if ref.startswith(tag_prefix):
r = ref[len(tag_prefix):]
if verbose:
print("picking %s" % r)
return {"version": r,
"full-revisionid": keywords["full"].strip(),
"dirty": False, "error": None,
"date": date}
# no suitable tags, so version is "0+unknown", but full hex is still there
if verbose:
print("no suitable tags, using unknown + full revision id")
return {"version": "0+unknown",
"full-revisionid": keywords["full"].strip(),
"dirty": False, "error": "no suitable tags", "date": None}
@register_vcs_handler("git", "pieces_from_vcs")
def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command):
"""Get version from 'git describe' in the root of the source tree.
This only gets called if the git-archive 'subst' keywords were *not*
expanded, and _version.py hasn't already been rewritten with a short
version string, meaning we're inside a checked out source tree.
"""
GITS = ["git"]
if sys.platform == "win32":
GITS = ["git.cmd", "git.exe"]
out, rc = run_command(GITS, ["rev-parse", "--git-dir"], cwd=root,
hide_stderr=True)
if rc != 0:
if verbose:
print("Directory %s not under git control" % root)
raise NotThisMethod("'git rev-parse --git-dir' returned error")
# 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)
# --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)
if full_out is None:
raise NotThisMethod("'git rev-parse' failed")
full_out = full_out.strip()
pieces = {}
pieces["long"] = full_out
pieces["short"] = full_out[:7] # maybe improved later
pieces["error"] = None
# parse describe_out. It will be like TAG-NUM-gHEX[-dirty] or HEX[-dirty]
# TAG might have hyphens.
git_describe = describe_out
# look for -dirty suffix
dirty = git_describe.endswith("-dirty")
pieces["dirty"] = dirty
if dirty:
git_describe = git_describe[:git_describe.rindex("-dirty")]
# now we have TAG-NUM-gHEX or HEX
if "-" in git_describe:
# TAG-NUM-gHEX
mo = re.search(r'^(.+)-(\d+)-g([0-9a-f]+)$', git_describe)
if not mo:
# unparseable. Maybe git-describe is misbehaving?
pieces["error"] = ("unable to parse git-describe output: '%s'"
% describe_out)
return pieces
# tag
full_tag = mo.group(1)
if not full_tag.startswith(tag_prefix):
if verbose:
fmt = "tag '%s' doesn't start with prefix '%s'"
print(fmt % (full_tag, tag_prefix))
pieces["error"] = ("tag '%s' doesn't start with prefix '%s'"
% (full_tag, tag_prefix))
return pieces
pieces["closest-tag"] = full_tag[len(tag_prefix):]
# distance: number of commits since tag
pieces["distance"] = int(mo.group(2))
# commit: short hex revision ID
pieces["short"] = mo.group(3)
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
# commit date: see ISO-8601 comment in git_versions_from_keywords()
date = run_command(GITS, ["show", "-s", "--format=%ci", "HEAD"],
cwd=root)[0].strip()
pieces["date"] = date.strip().replace(" ", "T", 1).replace(" ", "", 1)
return pieces
def plus_or_dot(pieces):
"""Return a + if we don't already have one, else return a ."""
if "+" in pieces.get("closest-tag", ""):
return "."
return "+"
def render_pep440(pieces):
"""Build up version string, with post-release "local version identifier".
Our goal: TAG[+DISTANCE.gHEX[.dirty]] . Note that if you
get a tagged build and then dirty it, you'll get TAG+0.gHEX.dirty
Exceptions:
1: no tags. git_describe was just HEX. 0+untagged.DISTANCE.gHEX[.dirty]
"""
if pieces["closest-tag"]:
rendered = pieces["closest-tag"]
if pieces["distance"] or pieces["dirty"]:
rendered += plus_or_dot(pieces)
rendered += "%d.g%s" % (pieces["distance"], pieces["short"])
if pieces["dirty"]:
rendered += ".dirty"
else:
# exception #1
rendered = "0+untagged.%d.g%s" % (pieces["distance"],
pieces["short"])
if pieces["dirty"]:
rendered += ".dirty"
return rendered
def render_pep440_pre(pieces):
"""TAG[.post.devDISTANCE] -- No -dirty.
Exceptions:
1: no tags. 0.post.devDISTANCE
"""
if pieces["closest-tag"]:
rendered = pieces["closest-tag"]
if pieces["distance"]:
rendered += ".post.dev%d" % pieces["distance"]
else:
# exception #1
rendered = "0.post.dev%d" % pieces["distance"]
return rendered
def render_pep440_post(pieces):
"""TAG[.postDISTANCE[.dev0]+gHEX] .
The ".dev0" means dirty. Note that .dev0 sorts backwards
(a dirty tree will appear "older" than the corresponding clean one),
but you shouldn't be releasing software with -dirty anyways.
Exceptions:
1: no tags. 0.postDISTANCE[.dev0]
"""
if pieces["closest-tag"]:
rendered = pieces["closest-tag"]
if pieces["distance"] or pieces["dirty"]:
rendered += ".post%d" % pieces["distance"]
if pieces["dirty"]:
rendered += ".dev0"
rendered += plus_or_dot(pieces)
rendered += "g%s" % pieces["short"]
else:
# exception #1
rendered = "0.post%d" % pieces["distance"]
if pieces["dirty"]:
rendered += ".dev0"
rendered += "+g%s" % pieces["short"]
return rendered
def render_pep440_old(pieces):
"""TAG[.postDISTANCE[.dev0]] .
The ".dev0" means dirty.
Eexceptions:
1: no tags. 0.postDISTANCE[.dev0]
"""
if pieces["closest-tag"]:
rendered = pieces["closest-tag"]
if pieces["distance"] or pieces["dirty"]:
rendered += ".post%d" % pieces["distance"]
if pieces["dirty"]:
rendered += ".dev0"
else:
# exception #1
rendered = "0.post%d" % pieces["distance"]
if pieces["dirty"]:
rendered += ".dev0"
return rendered
def render_git_describe(pieces):
"""TAG[-DISTANCE-gHEX][-dirty].
Like 'git describe --tags --dirty --always'.
Exceptions:
1: no tags. HEX[-dirty] (note: no 'g' prefix)
"""
if pieces["closest-tag"]:
rendered = pieces["closest-tag"]
if pieces["distance"]:
rendered += "-%d-g%s" % (pieces["distance"], pieces["short"])
else:
# exception #1
rendered = pieces["short"]
if pieces["dirty"]:
rendered += "-dirty"
return rendered
def render_git_describe_long(pieces):
"""TAG-DISTANCE-gHEX[-dirty].
Like 'git describe --tags --dirty --always -long'.
The distance/hash is unconditional.
Exceptions:
1: no tags. HEX[-dirty] (note: no 'g' prefix)
"""
if pieces["closest-tag"]:
rendered = pieces["closest-tag"]
rendered += "-%d-g%s" % (pieces["distance"], pieces["short"])
else:
# exception #1
rendered = pieces["short"]
if pieces["dirty"]:
rendered += "-dirty"
return rendered
def render(pieces, style):
"""Render the given version pieces into the requested style."""
if pieces["error"]:
return {"version": "unknown",
"full-revisionid": pieces.get("long"),
"dirty": None,
"error": pieces["error"],
"date": None}
if not style or style == "default":
style = "pep440" # the default
if style == "pep440":
rendered = render_pep440(pieces)
elif style == "pep440-pre":
rendered = render_pep440_pre(pieces)
elif style == "pep440-post":
rendered = render_pep440_post(pieces)
elif style == "pep440-old":
rendered = render_pep440_old(pieces)
elif style == "git-describe":
rendered = render_git_describe(pieces)
elif style == "git-describe-long":
rendered = render_git_describe_long(pieces)
else:
raise ValueError("unknown style '%s'" % style)
return {"version": rendered, "full-revisionid": pieces["long"],
"dirty": pieces["dirty"], "error": None,
"date": pieces.get("date")}
def get_versions():
"""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
# py2exe/bbfreeze/non-CPython implementations don't do __file__, in which
# case we can only use expanded keywords.
cfg = get_config()
verbose = cfg.verbose
try:
return git_versions_from_keywords(get_keywords(), cfg.tag_prefix,
verbose)
except NotThisMethod:
pass
try:
root = os.path.realpath(__file__)
# 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('/'):
root = os.path.dirname(root)
except NameError:
return {"version": "0+unknown", "full-revisionid": None,
"dirty": None,
"error": "unable to find root of source tree",
"date": None}
try:
pieces = git_pieces_from_vcs(cfg.tag_prefix, root, verbose)
return render(pieces, cfg.style)
except NotThisMethod:
pass
try:
if cfg.parentdir_prefix:
return versions_from_parentdir(cfg.parentdir_prefix, root, verbose)
except NotThisMethod:
pass
return {"version": "0+unknown", "full-revisionid": None,
"dirty": None,
"error": "unable to compute version", "date": None}
'''
sinfo finds and prints version information for loaded modules in the current
session, Python, and the OS.
'''
import sys
import types
import platform
import inspect
from datetime import datetime
from importlib import import_module
from inspect import cleandoc
from multiprocessing import cpu_count
from pathlib import Path
from stdlib_list import stdlib_list
def _imports(environ):
'''Find modules in an environment.'''
for name, val in environ:
# If the module was directly imported
if isinstance(val, types.ModuleType):
yield val.__name__
# If something was imported from the module
else:
try:
yield val.__module__.split('.')[0]
except AttributeError:
pass
def _find_version(mod_version_attr):
'''Find the version number of a module'''
if (isinstance(mod_version_attr, str)
or isinstance(mod_version_attr, int)):
return mod_version_attr
elif isinstance(mod_version_attr, tuple):
joined_tuple = '.'.join([str(num) for num in mod_version_attr])
return joined_tuple
elif callable(mod_version_attr):
return mod_version_attr()
else:
# print(f'Does not support module version of type {type(mod_ver_attr)}')
return 'NA'
# Straight from https://stackoverflow.com/a/52187331/2166823
# Slight modifications added
def _notebook_basename():
"""Returns the absolute path of the Notebook or None if it cannot be determined
NOTE: works only when the security is token-based or there is also no password
"""
import ipykernel
import json
import urllib
from notebook import notebookapp
connection_file = Path(ipykernel.get_connection_file()).name
kernel_id = connection_file.split('-', 1)[1].split('.')[0]
for srv in notebookapp.list_running_servers():
if srv['token'] == '' and not srv['password']: # No token and no password
req = urllib.request.urlopen(srv['url']+'api/sessions')
else:
req = urllib.request.urlopen(srv['url']+'api/sessions?token='+srv['token'])
sessions = json.load(req)
for sess in sessions:
if sess['kernel']['id'] == kernel_id:
return Path(sess['notebook']['path']).stem
return None
def sinfo(na=True, os=True, cpu=True, jupyter=None, dependencies=None,
std_lib=False, private=False, write_req_file=None, req_file_name=None,
html=False, excludes=['builtins', 'stdlib_list']):
'''
Output version information for loaded modules in the current session,
Python, and the OS.
Parameters
----------
na : bool
Output module name even when no version number is found.
os : bool
Output OS information.
cpu : bool
Output number of logical CPU cores and info string (if available).
jupyter : bool
Output information about the jupyter environment. If `None`, output
jupyter info only if inside a Jupyter notebook.
dependencies : bool
Output information about modules imported by the Python interpreter on
startup and depency modules imported via other modules. If `None`,
dependency modules will be included in the HTML output under a
<details> tag, and excluded from the printed output. Setting `na` to
`False` could be helpful to reduce verboseness.
std_lib : bool
Output information for modules imported from the standard library.
Tries to detect the Python version to compare with the corresponding
standard libarary, falls back to Python 3.7 if the version cannot be
detected.
private : bool
Output information for private modules.
write_req_file: bool
Create a pip-compatible text file that lists all the module versions.
If `None`, write dependency files for Jupyter notebooks only. Tries to
find the notebook name to prefix the requirments file, falls back to
`sinfo-requirements.txt`. Only writes explicitly imported modules.
req_file_name : str
Change the name of the requirements file.
html: bool
Format output as HTML and collapse it in a <details> tag. If `None`,
HTML will be used only if a Jupyter notebook environment is detected.
Note that this will not be visible in notebooks shared on GitHub since
they seemingly do not support the <details> tag. Requires IPython.
excludes : list
Do not output version information for these modules.
'''
# Exclude std lib packages
if not std_lib:
try:
std_modules = stdlib_list(version=platform.python_version()[:-2])
except ValueError:
# Use 3.7 if the Python version cannot be found
std_modules = stdlib_list('3.7')
excludes = excludes + std_modules
# Include jupyter info
in_notebook = 'jupyter_core' in sys.modules.keys()
if in_notebook:
if html is None:
html = True
if jupyter or (jupyter is None and in_notebook):
jupyter = True
jup_mod_names = ['IPython', 'jupyter_client', 'jupyter_core',
'jupyterlab', 'notebook']
jup_modules = []
for jup_mod_name in jup_mod_names:
try:
jup_modules.append(import_module(jup_mod_name))
except ModuleNotFoundError:
pass
# The length of `'jupyter_client'` is 14
# The spaces are added to create uniform whitespace in the output
# f-strings, which is needed to clean them with inspect.cleandoc
jup_mod_and_ver = [f' {module.__name__:14}\t{module.__version__}'
for module in jup_modules]
output_jupyter = '\n'.join(jup_mod_and_ver)
else:
output_jupyter = None
# Get `globals()` from the enviroment where the function is executed
caller_globals = dict(
inspect.getmembers(inspect.stack()[1][0]))["f_globals"]
# Find imported modules in the global namespace
imported_modules = set(_imports(caller_globals.items()))
# Wether to include dependency module not explicitly imported
all_modules = {'imported': imported_modules}
if dependencies is not False: # Default with HTML is to include deps
if html or dependencies:
dependencies = True # HTML default, used later for output strings
sys_modules = set(sys.modules.keys())
depend_modules = sys_modules.difference(imported_modules)
all_modules['depend'] = depend_modules
output_modules = {}
for mod_type in all_modules:
modules = all_modules[mod_type]
# Keep module basename only. Filter duplicates and excluded modules.
if private:
clean_modules = set(module.split('.')[0] for module in modules
if module.split('.')[0] not in excludes)
else: # Also filter private modules
clean_modules = set(module.split('.')[0] for module in modules
if module.split('.')[0] not in excludes
and not module.startswith('_'))
# Don't duplicate jupyter module output
if jupyter or in_notebook:
for jup_module in jup_modules:
if jup_module.__name__ in clean_modules:
clean_modules.remove(jup_module.__name__)
# Find version number. Return NA if a version string can't be found.
# This section is modified from the `py_session` package
mod_and_ver = []
mod_names = []
mod_versions = []
for mod_name in clean_modules:
mod_names.append(mod_name)
mod = sys.modules[mod_name]
# Since modules use different attribute names to store version info,
# try the most common ones.
try:
mod_version = _find_version(mod.__version__)
except AttributeError:
try:
mod_version = _find_version(mod.version)
except AttributeError:
try:
mod_version = _find_version(mod.VERSION)
except AttributeError:
mod_version = 'NA'
# print(f'Cannot find a version attribute for {mod}.')
mod_versions.append(mod_version)
max_name_len = max([len(mod_name) for mod_name in mod_names])
mod_and_ver = [f'{mod_name:{max_name_len}}\t{mod_version}'
for mod_name, mod_version in zip(mod_names, mod_versions)]
if not na:
mod_and_ver = [x for x in mod_and_ver if not x[-2:] == 'NA']
mod_and_ver = sorted(mod_and_ver)
output_modules[mod_type] = '\n '.join(mod_and_ver)
# Write requirements file for notebooks only by default
if write_req_file or (write_req_file is None and in_notebook):
if req_file_name is None:
# Only import notebook librarie if we are in the notebook
# Otherwise, running this multiple times would set `in_notebook=True`
if in_notebook:
try:
req_file_name = f'{_notebook_basename()}-requirements.txt'
except: # Fallback if the notebook name cannot be found
req_file_name = 'sinfo-requirements.txt'
else:
req_file_name = 'sinfo-requirements.txt'
# For NA modules, just include the latest, so no version number.
mods_na_removed = [mod_ver.replace('\tNA', '')
for mod_ver in output_modules['imported'].split('\n')]
if jupyter:
mods_req_file = mods_na_removed + jup_mod_and_ver
else:
mods_req_file = mods_na_removed
clean_mods_req_file = [mod_ver.replace(' ', '').replace('\t', '==')
for mod_ver in mods_req_file]
with open(req_file_name, 'w') as f:
for mod_to_req in clean_mods_req_file:
f.write('{}\n'.format(mod_to_req))
# Sys info
sys_output = 'Python ' + sys.version.replace('\n', '')
os_output = platform.platform() if os else None
if cpu:
if platform.processor() != '':
cpu_output = f'{cpu_count()} logical CPU cores, {platform.processor()}'
else:
cpu_output = f'{cpu_count()} logical CPU cores'
else:
cpu_output = None
date_output = 'Session information updated at {}'.format(
datetime.now().strftime('%Y-%m-%d %H:%M'))
# Output
# For proper formatting and exclusion of `-----` when `jupyter=False`.
nl = '\n'
output_jup_str = ('' if output_jupyter is None
else f'{nl} -----{nl}{output_jupyter}')
if html:
from IPython.display import HTML
if dependencies:
# Must be dedented to line up with the returned HTML.
# Otherwise `cleandoc()` does not work.
output_depend_str = f"""
-----
</pre>
<details>
<summary>Click to view dependency modules</summary>
<pre>
{output_modules['depend']}
</pre>
</details> <!-- seems like this ends pre, so might as well be explicit -->
<pre>"""
else:
output_depend_str = ''
return HTML(cleandoc(f"""
<details>
<summary>Click to view module versions</summary>
<pre>
-----
{output_modules['imported']}{output_depend_str}{output_jup_str}
-----
{sys_output}
{os_output}
{cpu_output}
-----
{date_output}
</pre>
</details>"""))
else:
if dependencies:
# Must be dedented to line up with the returned HTML.
# Otherwise `cleandoc()` does not work.
output_depend_str = f"""
-----
{output_modules['depend']}"""
else:
output_depend_str = ''
print(cleandoc(f"""
-----
{output_modules['imported']}{output_depend_str}{output_jup_str}
-----
{sys_output}
{os_output}
{cpu_output}
-----
{date_output}"""))
This diff is collapsed.
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment