diff --git a/setup.py b/setup.py index 5d6b734..c43a737 100755 --- a/setup.py +++ b/setup.py @@ -17,7 +17,6 @@ keywords="unification logic-programming dispatch", packages=["unification"], install_requires=[ - "toolz", "multipledispatch", ], long_description=(open("README.md").read() if exists("README.md") else ""), diff --git a/unification/_version.py b/unification/_version.py index bff98d1..e103428 100644 --- a/unification/_version.py +++ b/unification/_version.py @@ -1,4 +1,3 @@ - # 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 @@ -59,17 +58,18 @@ class NotThisMethod(Exception): def register_vcs_handler(vcs, method): # decorator """Create decorator to mark a method as the handler of a 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): +def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False, env=None): """Call the given command(s).""" assert isinstance(commands, list) process = None @@ -77,10 +77,13 @@ def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False, try: dispcmd = str([command] + args) # remember shell=False, so use git.cmd on windows, not just git - process = subprocess.Popen([command] + 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), + ) break except OSError: e = sys.exc_info()[1] @@ -115,15 +118,21 @@ def versions_from_parentdir(parentdir_prefix, root, verbose): 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} + return { + "version": dirname[len(parentdir_prefix) :], + "full-revisionid": None, + "dirty": False, + "error": None, + "date": None, + } 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)) + print( + "Tried directories %s but none started with prefix %s" + % (str(rootdirs), parentdir_prefix) + ) raise NotThisMethod("rootdir doesn't start with parentdir_prefix") @@ -182,7 +191,7 @@ def git_versions_from_keywords(keywords, tag_prefix, verbose): # 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 = {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 @@ -191,7 +200,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 = {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: @@ -199,24 +208,31 @@ def git_versions_from_keywords(keywords, tag_prefix, verbose): 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):] + 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): + if not re.match(r"\d", r): continue if verbose: print("picking %s" % r) - return {"version": r, - "full-revisionid": keywords["full"].strip(), - "dirty": False, "error": None, - "date": date} + 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} + return { + "version": "0+unknown", + "full-revisionid": keywords["full"].strip(), + "dirty": False, + "error": "no suitable tags", + "date": None, + } @register_vcs_handler("git", "pieces_from_vcs") @@ -233,8 +249,7 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, runner=run_command): GITS = ["git.cmd", "git.exe"] TAG_PREFIX_REGEX = r"\*" - _, rc = runner(GITS, ["rev-parse", "--git-dir"], cwd=root, - hide_stderr=True) + _, rc = runner(GITS, ["rev-parse", "--git-dir"], cwd=root, hide_stderr=True) if rc != 0: if verbose: print("Directory %s not under git control" % root) @@ -242,11 +257,19 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, runner=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 = runner(GITS, ["describe", "--tags", "--dirty", - "--always", "--long", - "--match", - "%s%s" % (tag_prefix, TAG_PREFIX_REGEX)], - cwd=root) + describe_out, rc = runner( + GITS, + [ + "describe", + "--tags", + "--dirty", + "--always", + "--long", + "--match", + "%s%s" % (tag_prefix, TAG_PREFIX_REGEX), + ], + cwd=root, + ) # --long was added in git-1.5.5 if describe_out is None: raise NotThisMethod("'git describe' failed") @@ -261,8 +284,7 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, runner=run_command): pieces["short"] = full_out[:7] # maybe improved later pieces["error"] = None - branch_name, rc = runner(GITS, ["rev-parse", "--abbrev-ref", "HEAD"], - cwd=root) + 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") @@ -302,17 +324,16 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, runner=run_command): dirty = git_describe.endswith("-dirty") pieces["dirty"] = dirty if dirty: - git_describe = git_describe[:git_describe.rindex("-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) + mo = re.search(r"^(.+)-(\d+)-g([0-9a-f]+)$", git_describe) if not mo: # unparsable. Maybe git-describe is misbehaving? - pieces["error"] = ("unable to parse git-describe output: '%s'" - % describe_out) + pieces["error"] = "unable to parse git-describe output: '%s'" % describe_out return pieces # tag @@ -321,10 +342,12 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, runner=run_command): 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)) + pieces["error"] = "tag '%s' doesn't start with prefix '%s'" % ( + full_tag, + tag_prefix, + ) return pieces - pieces["closest-tag"] = full_tag[len(tag_prefix):] + pieces["closest-tag"] = full_tag[len(tag_prefix) :] # distance: number of commits since tag pieces["distance"] = int(mo.group(2)) @@ -373,8 +396,7 @@ def render_pep440(pieces): rendered += ".dirty" else: # exception #1 - rendered = "0+untagged.%d.g%s" % (pieces["distance"], - pieces["short"]) + rendered = "0+untagged.%d.g%s" % (pieces["distance"], pieces["short"]) if pieces["dirty"]: rendered += ".dirty" return rendered @@ -403,8 +425,7 @@ def render_pep440_branch(pieces): rendered = "0" if pieces["branch"] != "master": rendered += ".dev0" - rendered += "+untagged.%d.g%s" % (pieces["distance"], - pieces["short"]) + rendered += "+untagged.%d.g%s" % (pieces["distance"], pieces["short"]) if pieces["dirty"]: rendered += ".dirty" return rendered @@ -432,7 +453,7 @@ def render_pep440_pre(pieces): 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"]) + rendered += ".post%d.dev%d" % (post_version + 1, pieces["distance"]) else: rendered += ".post0.dev%d" % (pieces["distance"]) else: @@ -565,11 +586,13 @@ def render_git_describe_long(pieces): 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} + 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 @@ -593,9 +616,13 @@ def render(pieces, style): else: raise ValueError("unknown style '%s'" % style) - return {"version": rendered, "full-revisionid": pieces["long"], - "dirty": pieces["dirty"], "error": None, - "date": pieces.get("date")} + return { + "version": rendered, + "full-revisionid": pieces["long"], + "dirty": pieces["dirty"], + "error": None, + "date": pieces.get("date"), + } def get_versions(): @@ -609,8 +636,7 @@ def get_versions(): verbose = cfg.verbose try: - return git_versions_from_keywords(get_keywords(), cfg.tag_prefix, - verbose) + return git_versions_from_keywords(get_keywords(), cfg.tag_prefix, verbose) except NotThisMethod: pass @@ -619,13 +645,16 @@ 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 _ 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, - "dirty": None, - "error": "unable to find root of source tree", - "date": None} + 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) @@ -639,6 +668,10 @@ def get_versions(): except NotThisMethod: pass - return {"version": "0+unknown", "full-revisionid": None, - "dirty": None, - "error": "unable to compute version", "date": None} + return { + "version": "0+unknown", + "full-revisionid": None, + "dirty": None, + "error": "unable to compute version", + "date": None, + } diff --git a/unification/core.py b/unification/core.py index cfd827b..ef31f17 100644 --- a/unification/core.py +++ b/unification/core.py @@ -1,6 +1,5 @@ from collections import OrderedDict, deque from collections.abc import Generator, Iterator, Mapping, Set -from copy import copy from functools import partial from operator import length_hint @@ -16,10 +15,7 @@ @dispatch(Mapping, object, object) def assoc(s, u, v): """Add an entry to a `Mapping` and return it.""" - if hasattr(s, "copy"): - s = s.copy() - else: - s = copy(s) # pragma: no cover + s = dict(s) s[u] = v return s @@ -35,27 +31,27 @@ def stream_eval(z, res_filter=None): if not isinstance(z, Generator): return z - stack = deque() + stack = deque([z]) z_args, z_out = None, None - stack.append(z) while stack: z = stack[-1] try: z_out = z.send(z_args) - if res_filter: - _ = res_filter(z, z_out) + if res_filter is not None: + res_filter(z, z_out) + + except StopIteration: + stack.pop() + else: if isinstance(z_out, Generator): stack.append(z_out) z_args = None else: z_args = z_out - except StopIteration: - _ = stack.pop() - return z_out diff --git a/unification/dispatch.py b/unification/dispatch.py index 4a5606e..b815c15 100644 --- a/unification/dispatch.py +++ b/unification/dispatch.py @@ -1,7 +1,7 @@ -from functools import partial +from functools import partial as _partial from multipledispatch import dispatch -namespace = dict() +namespace = {} -dispatch = partial(dispatch, namespace=namespace) +_partial = _partial(dispatch, namespace=namespace) diff --git a/unification/match.py b/unification/match.py index 8833ee5..f6722d5 100644 --- a/unification/match.py +++ b/unification/match.py @@ -1,14 +1,12 @@ -from toolz import first, groupby - from .core import reify, unify from .utils import _toposort, freeze from .variable import isvar -class Dispatcher(object): +class Dispatcher: def __init__(self, name): self.name = name - self.funcs = dict() + self.funcs = {} self.ordering = [] def add(self, signature, func): @@ -65,29 +63,25 @@ class VarDispatcher(Dispatcher): def __call__(self, *args, **kwargs): func, s = self.resolve(args) - d = dict((k.token, v) for k, v in s.items()) + d = {k.token: v for k, v in s.items()} return func(**d) -global_namespace = dict() +global_namespace = {} def match(*signature, **kwargs): namespace = kwargs.get("namespace", global_namespace) dispatcher = kwargs.get("Dispatcher", Dispatcher) - def _(func): + def _match(func): name = func.__name__ - if name not in namespace: - namespace[name] = dispatcher(name) - d = namespace[name] - + d = namespace.setdefault(name, dispatcher(name)) d.add(signature, func) - return d - return _ + return _match def supercedes(a, b): @@ -97,11 +91,8 @@ def supercedes(a, b): s = unify(a, b) if s is False: return False - s = dict((k, v) for k, v in s.items() if not isvar(k) or not isvar(v)) - if reify(a, s) == a: - return True - if reify(b, s) == b: - return False + s = {k: v for k, v in s.items() if not isvar(k) or not isvar(v)} + return reify(a, s) == a def edge(a, b, tie_breaker=hash): @@ -122,11 +113,6 @@ def ordering(signatures): Topological sort of edges as given by ``edge`` and ``supercedes`` """ - signatures = list(map(tuple, signatures)) - edges = [(a, b) for a in signatures for b in signatures if edge(a, b)] - edges = groupby(first, edges) - for s in signatures: - if s not in edges: - edges[s] = [] - edges = dict((k, [b for a, b in v]) for k, v in edges.items()) - return _toposort(edges) + return _toposort( + {s: [t for t in signatures if edge(s, t)] for s in map(tuple, signatures)} + ) diff --git a/unification/more.py b/unification/more.py index 071e638..1872bd0 100644 --- a/unification/more.py +++ b/unification/more.py @@ -100,11 +100,9 @@ def _unify_object(u, v, s): >>> unify_object(f, g, {}) {~x: 2} """ - if type(u) != type(v): + if type(u) is not type(v): yield False - return - - if hasattr(u, "__slots__"): + elif hasattr(u, "__slots__"): yield _unify( tuple(getattr(u, slot) for slot in u.__slots__), tuple(getattr(v, slot) for slot in v.__slots__), diff --git a/unification/utils.py b/unification/utils.py index 34735f4..91ae974 100644 --- a/unification/utils.py +++ b/unification/utils.py @@ -1,5 +1,8 @@ -from collections.abc import Mapping, Set -from contextlib import suppress +import sys +from collections import deque +from collections.abc import Mapping, Sequence, Set + +__PY37 = sys.version_info >= (3, 7) def transitive_get(key, d): @@ -11,9 +14,16 @@ def transitive_get(key, d): >>> transitive_get(1, d) 4 """ - with suppress(TypeError): - while key in d: + for _ in range(len(d) + 1): + try: + if key not in d: + break key = d[key] + except TypeError: + break + else: + raise RecursionError("dict contains a loop") + return key @@ -36,21 +46,23 @@ def _toposort(edges): Communications of the ACM [2] http://en.wikipedia.org/wiki/Toposort#Algorithms """ - incoming_edges = reverse_dict(edges) - incoming_edges = dict((k, set(val)) for k, val in incoming_edges.items()) - S = set((v for v in edges if v not in incoming_edges)) + incoming_edges = {k: set(val) for k, val in reverse_dict(edges).items()} + + S = deque(v for v in edges if v not in incoming_edges) L = [] while S: - n = S.pop() - L.append(n) + n = S.popleft() for m in edges.get(n, ()): - assert n in incoming_edges[m] - incoming_edges[m].remove(n) - if not incoming_edges[m]: - S.add(m) - if any(incoming_edges.get(v, None) for v in edges): + edges_m = incoming_edges[m] + edges_m.remove(n) + if not edges_m: + S.append(m) + L.append(n) + + if any(incoming_edges.get(v) for v in edges): raise ValueError("Input has cycles") + return L @@ -70,7 +82,7 @@ def reverse_dict(d): result = {} for key in d: for val in d[key]: - result[val] = result.get(val, tuple()) + (key,) + result[val] = result.get(val, ()) + (key,) return result @@ -86,10 +98,16 @@ def freeze(d): >>> freeze({1: 2}) # doctest: +SKIP ((1, 2),) """ + if isinstance(d, (str, bytes)): + return d if isinstance(d, Mapping): - return tuple(map(freeze, sorted(d.items(), key=lambda x: hash(x[0])))) + if __PY37: + items = d.items() + else: + items = sorted(d.items(), key=lambda x: hash(x[0])) + return tuple(map(freeze, items)) if isinstance(d, Set): return tuple(map(freeze, sorted(d, key=hash))) - if isinstance(d, (tuple, list)): + if isinstance(d, Sequence): return tuple(map(freeze, d)) return d diff --git a/unification/variable.py b/unification/variable.py index c7b3710..6d3ad43 100644 --- a/unification/variable.py +++ b/unification/variable.py @@ -1,15 +1,19 @@ import weakref from abc import ABCMeta -from contextlib import contextmanager, suppress +from contextlib import contextmanager -_global_logic_variables = set() -_glv = _global_logic_variables +_glv = _global_logic_variables = set() class LVarType(ABCMeta): def __instancecheck__(self, o): - with suppress(TypeError): - return issubclass(type(o), (Var, LVarType)) or o in _glv + if issubclass(type(o), Var): + return True + + try: + return o in _glv + except TypeError: + return False class Var(metaclass=LVarType): @@ -44,7 +48,7 @@ def __new__(cls, token=None, prefix=""): output. """ if token is None: - token = f"{prefix}_{Var._id}" + token = f"{prefix}_{cls._id}" cls._id += 1 obj = cls._refs.get(token, None) @@ -62,7 +66,7 @@ def __str__(self): __repr__ = __str__ def __eq__(self, other): - if type(self) == type(other): + if type(self) is type(other): return self.token == other.token return NotImplemented