Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 25 additions & 7 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,12 +1,27 @@
.PHONY: test flakes tags clean release official
.PHONY: test flakes lint format tags clean install sync build release official

all: test

install:
@uv sync

sync:
@uv sync --all-extras

test:
@attest -rquickfix
@uv run attest -rquickfix

flakes:
@pyflakes attest
@uv run ruff check attest

lint:
@uv run ruff check attest

format:
@uv run ruff format attest

fix:
@uv run ruff check --fix attest

tags:
@ctags -R attest
Expand All @@ -16,9 +31,12 @@ clean:
@echo
@echo | xargs -p git clean -fdx

release:
@python setup.py release sdist build_sphinx -Ea
build:
@uv build

release: build
@echo "Built distribution packages"

official:
@tox -e ALL
@echo | xargs -p python setup.py upload_docs release sdist upload
@uv run tox -e ALL
@echo "Run 'uv publish' to upload to PyPI"
47 changes: 24 additions & 23 deletions attest/ast.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
"""
ast
~~~
Expand Down Expand Up @@ -86,15 +85,13 @@ def literal_eval(node_or_string):
dicts, booleans or None.
"""
_safe_names = {'None': None, 'True': True, 'False': False}
if isinstance(node_or_string, basestring):
if isinstance(node_or_string, str):
node_or_string = parse(node_or_string, mode='eval')
if isinstance(node_or_string, Expression):
node_or_string = node_or_string.body
def _convert(node):
if isinstance(node, Str):
return node.s
elif isinstance(node, Num):
return node.n
if isinstance(node, Constant):
return node.value
elif isinstance(node, Tuple):
return tuple(map(_convert, node.elts))
elif isinstance(node, List):
Expand All @@ -120,21 +117,21 @@ def dump(node, annotate_fields=True, include_attributes=False):
def _format(node):
if isinstance(node, AST):
fields = [(a, _format(b)) for a, b in iter_fields(node)]
rv = '%s(%s' % (node.__class__.__name__, ', '.join(
('%s=%s' % field for field in fields)
if annotate_fields else
(b for a, b in fields)
))
if annotate_fields:
fields_str = ", ".join(f"{a}={b}" for a, b in fields)
else:
fields_str = ", ".join(b for a, b in fields)
rv = f'{node.__class__.__name__}({fields_str}'
if include_attributes and node._attributes:
rv += fields and ', ' or ' '
rv += ', '.join('%s=%s' % (a, _format(getattr(node, a)))
rv += ', '.join(f'{a}={_format(getattr(node, a))}'
for a in node._attributes)
return rv + ')'
elif isinstance(node, list):
return '[%s]' % ', '.join(_format(x) for x in node)
return f'[{", ".join(_format(x) for x in node)}]'
return repr(node)
if not isinstance(node, AST):
raise TypeError('expected AST, got %r' % node.__class__.__name__)
raise TypeError(f'expected AST, got {node.__class__.__name__!r}')
return _format(node)


Expand Down Expand Up @@ -230,10 +227,14 @@ def get_docstring(node, trim=True):
will be raised.
"""
if not isinstance(node, (FunctionDef, ClassDef, Module)):
raise TypeError("%r can't have docstrings" % node.__class__.__name__)
if node.body and isinstance(node.body[0], Expr) and \
isinstance(node.body[0].value, Str):
doc = node.body[0].value.s
raise TypeError(f"{node.__class__.__name__!r} can't have docstrings")
if (
node.body
and isinstance(node.body[0], Expr)
and isinstance(node.body[0].value, Constant)
and isinstance(node.body[0].value.value, str)
):
doc = node.body[0].value.value
if trim:
doc = trim_docstring(doc)
return doc
Expand All @@ -244,8 +245,8 @@ def trim_docstring(docstring):
lines = docstring.expandtabs().splitlines()

# Find minimum indentation of any non-blank lines after first line.
from sys import maxint
margin = maxint
from sys import maxsize
margin = maxsize
for line in lines[1:]:
content = len(line.lstrip())
if content:
Expand All @@ -255,7 +256,7 @@ def trim_docstring(docstring):
# Remove indentation.
if lines:
lines[0] = lines[0].lstrip()
if margin < maxint:
if margin < maxsize:
for i in range(1, len(lines)):
lines[i] = lines[i][margin:]

Expand All @@ -274,7 +275,7 @@ def get_symbol(operator):
try:
return ALL_SYMBOLS[operator]
except KeyError:
raise LookupError('no known symbol for %r' % operator)
raise LookupError(f'no known symbol for {operator!r}')


def walk(node):
Expand All @@ -290,7 +291,7 @@ def walk(node):
yield node


class NodeVisitor(object):
class NodeVisitor:
"""Walks the abstract syntax tree and call visitor functions for every
node found. The visitor functions may return values which will be
forwarded by the `visit` method.
Expand Down
49 changes: 13 additions & 36 deletions attest/codegen.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
"""
codegen
~~~~~~~
Expand Down Expand Up @@ -59,7 +58,7 @@ def write(self, x):
def newline(self, node=None, extra=0):
self.new_lines = max(self.new_lines, 1 + extra)
if node is not None and self.add_line_information:
self.write('# line: %s' % node.lineno)
self.write(f'# line: {node.lineno}')
self.new_lines = 1

def body(self, statements):
Expand Down Expand Up @@ -127,7 +126,7 @@ def visit_AugAssign(self, node):

def visit_ImportFrom(self, node):
self.newline(node)
self.write('from %s%s import ' % ('.' * node.level, node.module))
self.write(f'from {"." * node.level}{node.module} import ')
for idx, item in enumerate(node.names):
if idx:
self.write(', ')
Expand All @@ -147,7 +146,7 @@ def visit_FunctionDef(self, node):
self.newline(extra=1)
self.decorators(node)
self.newline(node)
self.write('def %s(' % node.name)
self.write(f'def {node.name}(')
self.signature(node.args)
self.write('):')
self.body(node.body)
Expand All @@ -164,7 +163,7 @@ def paren_or_comma():
self.newline(extra=2)
self.decorators(node)
self.newline(node)
self.write('class %s' % node.name)
self.write(f'class {node.name}')
for base in node.bases:
paren_or_comma()
self.visit(base)
Expand Down Expand Up @@ -237,23 +236,6 @@ def visit_Pass(self, node):
self.newline(node)
self.write('pass')

def visit_Print(self, node):
# XXX: python 2.6 only
self.newline(node)
self.write('print ')
want_comma = False
if node.dest is not None:
self.write(' >> ')
self.visit(node.dest)
want_comma = True
for value in node.values:
if want_comma:
self.write(', ')
self.visit(value)
want_comma = True
if not node.nl:
self.write(',')

def visit_Delete(self, node):
self.newline(node)
self.write('del ')
Expand Down Expand Up @@ -341,11 +323,11 @@ def write_comma():
write_comma()
self.write(keyword.arg + '=')
self.visit(keyword.value)
if node.starargs is not None:
if hasattr(node, 'starargs') and node.starargs is not None:
write_comma()
self.write('*')
self.visit(node.starargs)
if node.kwargs is not None:
if hasattr(node, 'kwargs') and node.kwargs is not None:
write_comma()
self.write('**')
self.visit(node.kwargs)
Expand All @@ -354,14 +336,8 @@ def write_comma():
def visit_Name(self, node):
self.write(node.id)

def visit_Str(self, node):
self.write(repr(node.s))

def visit_Bytes(self, node):
self.write(repr(node.s))

def visit_Num(self, node):
self.write(repr(node.n))
def visit_Constant(self, node):
self.write(repr(node.value))

def visit_Tuple(self, node):
self.write('(')
Expand Down Expand Up @@ -399,23 +375,23 @@ def visit_Dict(self, node):
def visit_BinOp(self, node):
self.write('(')
self.visit(node.left)
self.write(' %s ' % BINOP_SYMBOLS[type(node.op)])
self.write(f' {BINOP_SYMBOLS[type(node.op)]} ')
self.visit(node.right)
self.write(')')

def visit_BoolOp(self, node):
self.write('(')
for idx, value in enumerate(node.values):
if idx:
self.write(' %s ' % BOOLOP_SYMBOLS[type(node.op)])
self.write(f' {BOOLOP_SYMBOLS[type(node.op)]} ')
self.visit(value)
self.write(')')

def visit_Compare(self, node):
self.write('(')
self.visit(node.left)
for op, right in zip(node.ops, node.comparators):
self.write(' %s ' % CMPOP_SYMBOLS[type(op)])
self.write(f' {CMPOP_SYMBOLS[type(op)]} ')
self.visit(right)
self.write(')')

Expand Down Expand Up @@ -549,4 +525,5 @@ def visit_Assert(self, node):

if __name__ == '__main__':
import sys
print to_source(parse(open(sys.argv[1]).read()))
from attest.ast import parse
print(to_source(parse(open(sys.argv[1]).read())))
20 changes: 9 additions & 11 deletions attest/collectors.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
# coding:utf-8
from __future__ import with_statement

import inspect
import re
import sys
Expand All @@ -23,7 +20,7 @@
]


class Tests(object):
class Tests:
"""Collection of test functions.

:param tests:
Expand Down Expand Up @@ -54,7 +51,7 @@ class Tests(object):
def __init__(self, tests=(), contexts=None,
replace_tests=False, replace_contexts=False):
self._tests = []
if isinstance(tests, basestring):
if isinstance(tests, str):
self.register(tests)
else:
for collection in tests:
Expand Down Expand Up @@ -87,7 +84,8 @@ def test(self, func):
def wrapper():
with nested(self._contexts) as context:
context = [c for c in context if c is not None]
argc = len(inspect.getargspec(func)[0])
sig = inspect.signature(func)
argc = len(sig.parameters)
args = []
for arg in context:
if type(arg) is tuple: # type() is intentional
Expand Down Expand Up @@ -205,7 +203,7 @@ def register(self, tests):
if inspect.isclass(tests):
self._tests.extend(tests())
return tests
elif isinstance(tests, basestring):
elif isinstance(tests, str):
def istests(obj):
return isinstance(obj, Tests)
obj = import_dotted_name(tests)
Expand Down Expand Up @@ -264,10 +262,10 @@ def test_case(self):
if not name:
name = "unnamed"
if not name.startswith("test_"):
name = "test_%s" % (name, )
name = f"test_{name}"
count = counts.increment(name)
if count > 1:
name = "%s_%s" % (name, count)
name = f"{name}_{count}"
methods[name] = staticmethod(func)
return type("Tests", (TestCase, ), methods)

Expand Down Expand Up @@ -314,7 +312,7 @@ def run(self, reporter=auto_reporter,
raise
else:
break
except BaseException, e:
except BaseException as e:
result.time = time() - result.time
result.error = e
result.stdout, result.stderr = out, err
Expand Down Expand Up @@ -388,7 +386,7 @@ def wrapper(self):
return wrapper


class TestBase(object):
class TestBase:
"""Base for test classes. Decorate test methods with :func:`test`. Needs
to be registered with a :class:`Tests` collection to be run. For setup
and teardown, override :meth:`__context__` like a
Expand Down
Loading