diff --git a/README.md b/README.md index 2d97084..4275317 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ Install using pip (>= 19.0). ```bash pip install --upgrade pip -pip install git+https://github.com/cms-l1-globaltrigger/tm-diff.git@0.7.3 +pip install git+https://github.com/cms-l1-globaltrigger/tm-diff.git@0.8.0 ``` ## Basic usage diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..fed528d --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["setuptools"] +build-backend = "setuptools.build_meta" diff --git a/requirements.txt b/requirements.txt index 7c594b0..65fa539 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1 @@ -tm-python @ git+https://github.com/cms-l1-globaltrigger/tm-python@0.10.0 +tm-python @ git+https://github.com/cms-l1-globaltrigger/tm-python@0.11.0 diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..2c0addb --- /dev/null +++ b/setup.cfg @@ -0,0 +1,31 @@ +[metadata] +name = tm-diff +version = attr: tmDiff.__version__ +author = Bernhard Arnold +author_email = bernhard.arnold@cern.ch +description = Compare the content of two XML trigger menus. +long_description = file: README.md +long_description_content_type = text/markdown +license = GPLv3 +classifiers = + "Topic :: Software Development" + "Topic :: Utilities" + +[options] +python_requires = >=3.6 +packages = find: +install_requires = + tm-python @ git+https://github.com/cms-l1-globaltrigger/tm-python@0.11.0 +test_suite = tests + +[options.packages.find] +exclude=tests + +[options.entry_points] +console_scripts = + tm-diff = tmDiff.__main__:main + +[mypy] + +[mypy-tmTable.*] +ignore_missing_imports = True diff --git a/setup.py b/setup.py deleted file mode 100644 index f8897c8..0000000 --- a/setup.py +++ /dev/null @@ -1,30 +0,0 @@ -from setuptools import setup - -long_description = open('README.md').read() - -setup( - name="tm-diff", - version='0.7.3', - url="https://github.com/cms-l1-globaltrigger/tm-diff", - author="Bernhard Arnold", - author_email="bernhard.arnold@cern.ch", - description="Compare the content of two XML trigger menus.", - long_description=long_description, - packages=['tmDiff'], - install_requires=[ - 'tm-python @ git+https://github.com/cms-l1-globaltrigger/tm-python@0.10.0', - ], - entry_points={ - 'console_scripts': [ - 'tm-diff = tmDiff.__main__:main', - ], - }, - test_suite='tests', - license="GPLv3", - keywords="", - platforms="any", - classifiers=[ - "Topic :: Software Development", - "Topic :: Utilities", - ] -) diff --git a/tmDiff/__init__.py b/tmDiff/__init__.py index 1ef1319..777f190 100644 --- a/tmDiff/__init__.py +++ b/tmDiff/__init__.py @@ -1 +1 @@ -__version__ = '0.7.3' +__version__ = "0.8.0" diff --git a/tmDiff/__main__.py b/tmDiff/__main__.py index d32c20d..c56321b 100755 --- a/tmDiff/__main__.py +++ b/tmDiff/__main__.py @@ -1,102 +1,106 @@ import argparse -import sys, os +import os +import sys +from typing import Callable, Dict, List from . import menudiff from . import __version__ -FMT_UNIFIED = 'unified' -FMT_CONTEXT = 'context' -FMT_HTML = 'html' -FMT_REPORT = 'report' -FMT_DEFAULT = FMT_UNIFIED -FMT_CHOICES = [FMT_UNIFIED, FMT_CONTEXT, FMT_HTML, FMT_REPORT] - -SKIP_MODULE = 'module' -SKIP_COMMENT = 'comment' -SKIP_LABELS = 'labels' -SKIP_CHOICES = [SKIP_MODULE, SKIP_COMMENT, SKIP_LABELS] - -SORT_INDEX = 'index' -SORT_NAME = 'name' -SORT_EXPRESSION = 'expression' -SORT_DEFAULT = SORT_INDEX -SORT_CHOICES = [SORT_INDEX, SORT_NAME, SORT_EXPRESSION] - -DIFF_FUNCTIONS = { +FMT_UNIFIED: str = "unified" +FMT_CONTEXT: str = "context" +FMT_HTML: str = "html" +FMT_REPORT: str = "report" +FMT_DEFAULT: str = FMT_UNIFIED +FMT_CHOICES: List[str] = [FMT_UNIFIED, FMT_CONTEXT, FMT_HTML, FMT_REPORT] + +SKIP_MODULE: str = "module" +SKIP_COMMENT: str = "comment" +SKIP_LABELS: str = "labels" +SKIP_CHOICES: List[str] = [SKIP_MODULE, SKIP_COMMENT, SKIP_LABELS] + +SORT_INDEX: str = "index" +SORT_NAME: str = "name" +SORT_EXPRESSION: str = "expression" +SORT_DEFAULT: str = SORT_INDEX +SORT_CHOICES: List[str] = [SORT_INDEX, SORT_NAME, SORT_EXPRESSION] + +DIFF_FUNCTIONS: Dict[str, Callable] = { FMT_UNIFIED: menudiff.unified_diff, FMT_CONTEXT: menudiff.context_diff, FMT_HTML: menudiff.html_diff, FMT_REPORT: menudiff.report_diff, } -def parse_args(): + +def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser() - parser.add_argument('file', + parser.add_argument("file", nargs=2, help="XML menu files 'FILE1 FILE2'" ) - parser.add_argument('-f', '--format', - metavar='', + parser.add_argument("-f", "--format", + metavar="", choices=FMT_CHOICES, default=FMT_DEFAULT, - help="select output format, default is '{0}'".format(FMT_DEFAULT) + help=f"select output format, default is '{FMT_DEFAULT}'" ) - parser.add_argument('-s', '--skip', - metavar='', - action='append', + parser.add_argument("-s", "--skip", + metavar="", + action="append", choices=SKIP_CHOICES, default=[], help="skip information" ) - parser.add_argument('--sort', - metavar='', + parser.add_argument("--sort", + metavar="", choices=SORT_CHOICES, default=SORT_DEFAULT, - help="select key for algorithm sorting, default is '{0}'".format(SORT_DEFAULT) + help=f"select key for algorithm sorting, default is '{SORT_DEFAULT}'" ) - parser.add_argument('-d', '--dump', - action='store_true', - help="dump the extracted intermediate content" + parser.add_argument("-d", "--dump", + action="store_true", + help="dump the extracted intermediate content" ) - parser.add_argument('-o', - dest='ostream', - metavar='', - type=argparse.FileType('w'), + parser.add_argument("-o", + dest="ostream", + metavar="", + type=argparse.FileType("wt"), default=sys.stdout, help="write output to file" ) - parser.add_argument('-v', '--verbose', - action='count', + parser.add_argument("-v", "--verbose", + action="count", help="increase output verbosity" ) - parser.add_argument('--version', - action='version', + parser.add_argument("--version", + action="version", version="%(prog)s {0}".format(__version__) ) return parser.parse_args() -def main(): + +def main() -> int: args = parse_args() - from_file = args.file[0] - to_file = args.file[1] + from_file: str = args.file[0] + to_file: str = args.file[1] - skip = [] + skip: List[str] = [] # Skip module specific attributes if SKIP_MODULE in args.skip: - skip.append('uuid_firmware') - skip.append('n_modules') - skip.append('module_id') - skip.append('module_index') + skip.append("uuid_firmware") + skip.append("n_modules") + skip.append("module_id") + skip.append("module_index") # Skip comments if SKIP_COMMENT in args.skip: - skip.append('comment') + skip.append("comment") # Skip comments if SKIP_LABELS in args.skip: - skip.append('labels') + skip.append("labels") # Extract information from XMLs from_menu = menudiff.Menu(from_file) @@ -121,5 +125,6 @@ def main(): return 0 -if __name__ == '__main__': + +if __name__ == "__main__": sys.exit(main()) diff --git a/tmDiff/menudiff.py b/tmDiff/menudiff.py index f6a2e04..c09ea3b 100644 --- a/tmDiff/menudiff.py +++ b/tmDiff/menudiff.py @@ -24,141 +24,151 @@ import difflib import os import sys +from typing import Any, Dict, List, Optional, TextIO, Tuple import tmTable from . import __version__ + class TTY: """TTY escape codes.""" - clear = "\033[0m" - red = "\033[31m" - green = "\033[32m" - yellow = "\033[33m" - blue = "\033[34m" - magenta = "\033[35m" + + clear: str = "\033[0m" + red: str = "\033[31m" + green: str = "\033[32m" + yellow: str = "\033[33m" + blue: str = "\033[34m" + magenta: str = "\033[35m" + class Diffable: """Implements a diffabel object. To be inherited by classes defining class attribute `sorted_attribuites`. """ - sorted_attributes = tuple() + sorted_attributes: List[str] = [] - default_value = '' + default_value: str = "" - def __init__(self, **kwargs): + def __init__(self, **kwargs) -> None: for attr in self.sorted_attributes: setattr(self, attr, kwargs[attr] if attr in kwargs else self.default_value) - def fmt_attr(self, attr): + def fmt_attr(self, attr: str) -> str: """Format attribute used for unified diff. - >>> o.fmt_attr('foobar') + >>> o.fmt_attr("foobar") 'foobar: 42' """ return "{0}: {1}".format(attr, getattr(self, attr)) - def to_diff(self, skip=None): + def to_diff(self, skip: Optional[List[str]] = None) -> List[str]: """Returns diff-able list of attributes for unified diff. >>> o.to_diff() ['foo: 42', 'bar: baz'] - >>> o.to_diff(skip=['bar']) # skip attributes + >>> o.to_diff(skip=["bar"]) # skip attributes ['foo: 42'] """ - skip = skip or [] + if skip is None: + skip = [] return [self.fmt_attr(attr) for attr in self.sorted_attributes if attr not in skip] + class Meta(Diffable): """Diffable menu metadata container.""" - sorted_attributes = ( - 'name', - 'uuid_menu', - 'uuid_firmware', - 'n_modules', - 'grammar_version', - 'is_valid', - 'is_obsolete', - 'comment', - ) + sorted_attributes = [ + "name", + "uuid_menu", + "uuid_firmware", + "n_modules", + "grammar_version", + "is_valid", + "is_obsolete", + "comment", + ] + class Algorithm(Diffable): """Diffable algorithm container.""" - sorted_attributes = ( - 'index', - 'module_id', - 'module_index', - 'name', - 'expression', - 'comment', - 'labels', - ) + sorted_attributes = [ + "index", + "module_id", + "module_index", + "name", + "expression", + "comment", + "labels", + ] + + report_attributes: List[str] = [ + "index", + "name", + "expression", + "labels", + ] - report_attributes = ( - 'index', - 'name', - 'expression', - 'labels', - ) class Cut(Diffable): """Diffable cut container.""" - sorted_attributes = ( - 'name', - 'type', - 'object', - 'minimum', - 'maximum', - 'data', - 'comment', - ) + sorted_attributes = [ + "name", + "type", + "object", + "minimum", + "maximum", + "data", + "comment", + ] + class Menu: """Simple menu container.""" - def __init__(self, filename): + def __init__(self, filename: str) -> None: self.load(filename) - self.skip = [] # list of attributes to skip - self.sort = 'index' # sort key for algorithms + self.skip: List[str] = [] # list of attributes to skip + self.sort: str = "index" # sort key for algorithms - def load(self, filename): + def load(self, filename: str) -> None: """Load menu from XML file.""" self.filename = filename menu = tmTable.Menu() scale = tmTable.Scale() ext_signal = tmTable.ExtSignal() - message = tmTable.xml2menu(filename, menu, scale, ext_signal) + message: str = tmTable.xml2menu(filename, menu, scale, ext_signal) if message: raise RuntimeError(message) # Collect list of metadata - self.meta = Meta(**menu.menu) + self.meta: Meta = Meta(**menu.menu) # Collect list of algorithms and cuts - self.algorithms = [] + self.algorithms: List[Algorithm] = [] cuts = {} for row in menu.algorithms: self.algorithms.append(Algorithm(**row)) - if row['name'] in menu.cuts.keys(): - for cut in menu.cuts[row['name']]: - cuts[cut['name']] = Cut(**cut) - self.cuts = cuts.values() + if row["name"] in menu.cuts.keys(): + for cut in menu.cuts[row["name"]]: + cuts[cut["name"]] = Cut(**cut) + self.cuts: List[Cut] = list(cuts.values()) - def sorted_algorithms(self): + def sorted_algorithms(self) -> List[Algorithm]: """Returns sorted list of algorithms.""" - def sort_key(algorithm): - if self.sort == 'index': - return int(algorithm.index) - return getattr(algorithm, self.sort) + def sort_key(algorithm: Algorithm) -> Any: + value = getattr(algorithm, self.sort) + if self.sort == "index": + return int(value) + return value return sorted(self.algorithms, key=sort_key) - def sorted_cuts(self): + def sorted_cuts(self) -> List[Cut]: """Returns sorted list of cuts.""" - return sorted(self.cuts, key=lambda cut: cut.name) + return sorted(self.cuts, key=lambda cut: getattr(cut, "name")) - def to_diff(self): + def to_diff(self) -> List[str]: """Returns list of attributes to be read by unified_diff.""" - items = [] + items: List[str] = [] # Metadata items.extend(self.meta.to_diff(skip=self.skip)) # Algorithms @@ -171,39 +181,40 @@ def to_diff(self): items.extend(cut.to_diff(skip=self.skip)) return items - def dump_intermediate(self, outdir=None): + def dump_intermediate(self, outdir: Optional[str] = None) -> None: """Dumps intermediate text file used to perform the unified diff.""" if not outdir: outdir = os.getcwd() filename = "{0}.txt".format(os.path.basename(self.filename)) - with open(os.path.join(outdir, filename), 'w') as fp: + with open(os.path.join(outdir, filename), "wt") as fp: for line in self.to_diff(): fp.write(line) fp.write(os.linesep) -def report_diff(fromfile, tofile, verbose=False, ostream=sys.stdout): + +def report_diff(fromfile: Menu, tofile: Menu, verbose: bool = False, ostream: Optional[TextIO] = None) -> None: """Perform simple diff on two menus in TWiki format for reports. >>> report_diff(fromfile, tofile) """ - from_algorithms = {} - to_algorithms = {} + from_algorithms: Dict = {} + to_algorithms: Dict = {} for algorithm in fromfile.algorithms: - from_algorithms[algorithm.name] = algorithm + from_algorithms[getattr(algorithm, "name")] = algorithm for algorithm in tofile.algorithms: - to_algorithms[algorithm.name] = algorithm + to_algorithms[getattr(algorithm, "name")] = algorithm def added_algorithms(a, b): algorithms = [] - names = [algorithm.name for algorithm in b.algorithms] + names = [getattr(algorithm, "name") for algorithm in b.algorithms] for algorithm in a.algorithms: - if algorithm.name not in names: + if getattr(algorithm, "name") not in names: algorithms.append(algorithm) return algorithms - added = added_algorithms(tofile, fromfile) - removed = added_algorithms(fromfile, tofile) - updated = [] + added: List[Algorithm] = added_algorithms(tofile, fromfile) + removed: List[Algorithm] = added_algorithms(fromfile, tofile) + updated: List[Tuple] = [] for name, fromalgorithm in from_algorithms.items(): if name in to_algorithms: @@ -213,24 +224,27 @@ def added_algorithms(a, b): if getattr(fromalgorithm, attr) != getattr(toalgorithm, attr): differences.append([attr, getattr(fromalgorithm, attr), getattr(toalgorithm, attr)]) if differences: - updated.append([toalgorithm, differences]) + updated.append((toalgorithm, differences)) + + if ostream is None: + ostream = sys.stdout if added or removed or updated: - ostream.write("---++ Changes with respect to !{0}".format(fromfile.meta.name)) + ostream.write("---++ Changes with respect to !{0}".format(getattr(fromfile.meta, "name"))) ostream.write(os.linesep) if added: ostream.write(" * Added the following algorithms") ostream.write(os.linesep) for algorithm in added: - ostream.write(" * {0}".format(algorithm.name)) + ostream.write(" * {0}".format(getattr(algorithm, "name"))) ostream.write(os.linesep) if updated: ostream.write(" * Changed the following algorithms") ostream.write(os.linesep) for algorithm, differnces in updated: - ostream.write(" * {0}".format(algorithm.name)) + ostream.write(" * {0}".format(getattr(algorithm, "name"))) ostream.write(os.linesep) # Verbose changes if verbose: @@ -242,16 +256,20 @@ def added_algorithms(a, b): ostream.write(" * Removed the following algorithms") ostream.write(os.linesep) for algorithm in removed: - ostream.write(" * {0}".format(algorithm.name)) + ostream.write(" * {0}".format(getattr(algorithm, "name"))) ostream.write(os.linesep) -def unified_diff(fromfile, tofile, verbose=False, ostream=sys.stdout): + +def unified_diff(fromfile: Menu, tofile: Menu, verbose: bool = False, ostream: Optional[TextIO] = None) -> None: """Perform unified diff on two menus. >>> unified_diff(fromfile, tofile) """ fromlines = fromfile.to_diff() tolines = tofile.to_diff() + if ostream is None: + ostream = sys.stdout + def write_added(line): if ostream.isatty(): ostream.write(TTY.green) @@ -276,18 +294,18 @@ def write_marker(line): def write_match(line): ostream.write(line) - count = 0 + count: int = 0 for line in difflib.unified_diff(fromlines, tolines, fromfile=fromfile.filename, tofile=tofile.filename, lineterm=""): if count: ostream.write(os.linesep) # Print added lines - if line.startswith('+'): + if line.startswith("+"): write_added(line) # Print removed lines - elif line.startswith('-'): + elif line.startswith("-"): write_removed(line) # Print diff markers - elif line.startswith('@@'): + elif line.startswith("@@"): write_marker(line) # Print matching lines else: @@ -298,13 +316,17 @@ def write_match(line): if count: ostream.write(os.linesep) -def context_diff(fromfile, tofile, verbose=False, ostream=sys.stdout): + +def context_diff(fromfile: Menu, tofile: Menu, verbose: bool = False, ostream: Optional[TextIO] = None) -> None: """Perform context diff on two menus. >>> context_diff(fromfile, tofile) """ fromlines = fromfile.to_diff() tolines = tofile.to_diff() + if ostream is None: + ostream = sys.stdout + def write_added(line): if ostream.isatty(): ostream.write(TTY.green) @@ -336,21 +358,21 @@ def write_marker(line): def write_match(line): ostream.write(line) - count = 0 + count: int = 0 for line in difflib.context_diff(fromlines, tolines, fromfile=fromfile.filename, tofile=tofile.filename, lineterm=""): if count: ostream.write(os.linesep) # Print added lines - if line.startswith('+ '): + if line.startswith("+ "): write_added(line) # Print removed lines - elif line.startswith('- '): + elif line.startswith("- "): write_removed(line) # Print changed lines - elif line.startswith('! '): + elif line.startswith("! "): write_changed(line) # Print diff markers - elif line.startswith('---') or line.startswith('***'): + elif line.startswith("---") or line.startswith("***"): write_marker(line) # Print matching lines else: @@ -361,27 +383,28 @@ def write_match(line): if count: ostream.write(os.linesep) -def html_diff(fromfile, tofile, verbose=False, ostream=sys.stdout): + +def html_diff(fromfile: Menu, tofile: Menu, verbose: bool = False, ostream: Optional[TextIO] = None) -> None: """Perform diff on two menus and writes results to HTML table. - >>> with open("sample.html", "w") as f: + >>> with open("sample.html", "wt") as f: ... html_diff(fromfile, tofile, f) """ fromlines = fromfile.to_diff() tolines = tofile.to_diff() diff = difflib.HtmlDiff() - html = diff.make_file( + html: str = diff.make_file( fromlines, tolines, os.path.basename(fromfile.filename), os.path.basename(tofile.filename), ) - timestamp = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S') + timestamp: str = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") # patching CSS style and adding a footer - patches = [ - [ - """""", + patches: List[Tuple[str, str]] = [ + ( + "", """ /* additional styles added by tm-diff */ table:first-of-type { @@ -408,19 +431,22 @@ def html_diff(fromfile, tofile, verbose=False, ostream=sys.stdout): } """ - ], - [ - """""", + ), + ( + "", """ """.format(__version__, timestamp) - ], + ), ] for needle, patch in patches: html = html.replace(needle, patch, 1) # patch only first occurence + if ostream is None: + ostream = sys.stdout + ostream.write(html) diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..9eaea4f --- /dev/null +++ b/tox.ini @@ -0,0 +1,16 @@ +[tox] +envlist = py36, py37, py38, py39, py310, py311 +isolated_build = true +skip_missing_interpreters = true + +[testenv] +deps = + flake8 + pylint + mypy + pytest +commands = + flake8 tmDiff --select=E9,F63,F7,F82 + pylint -E tmDiff + mypy tmDiff + # pytest