diff --git a/.coveragerc b/.coveragerc new file mode 100644 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,2 @@ +[run] +branch = True diff --git a/.hgignore b/.hgignore new file mode 100644 --- /dev/null +++ b/.hgignore @@ -0,0 +1,96 @@ +syntax: glob +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.cache +nosetests.xml +coverage.xml + +# Translations +*.mo +*.pot + +# Django stuff: +*.log + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + + +# cache files for sublime text +*.tmlanguage.cache +*.tmPreferences.cache +*.stTheme.cache + +# workspace files are user-specific +*.sublime-workspace + +# project files should be checked into the repository, unless a significant +# proportion of contributors will probably not be using SublimeText +# *.sublime-project + +# sftp configuration file +sftp-config.json + + +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear on external disk +.Spotlight-V100 +.Trashes + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + + diff --git a/README.rst b/README.rst new file mode 100644 diff --git a/data.py b/prototype/data.py rename from data.py rename to prototype/data.py diff --git a/nobilis.py b/prototype/nobilis.py rename from nobilis.py rename to prototype/nobilis.py diff --git a/setup.py b/setup.py new file mode 100644 --- /dev/null +++ b/setup.py @@ -0,0 +1,47 @@ +"""A setuptools based setup module.""" + +# Always prefer setuptools over distutils +from setuptools import setup, find_packages +# To use a consistent encoding +from codecs import open as c_open +from os import path + +HERE = path.abspath(path.dirname(__file__)) + +# Get the long description from the README file +with c_open(path.join(HERE, 'README.rst'), encoding='utf-8') as f: + LONG_DESCRIPTION = f.read() + +setup( + name='nobilis', + + version='0.1.0', + + description='Nobilis character creator', + long_description=LONG_DESCRIPTION, + + # url='https://github.com/mwchase/class-namespaces', + + author='Max Woerner Chase', + author_email='max.chase@gmail.com', + + license='MIT', + + classifiers=[ + 'Development Status :: 3 - Alpha', + + 'License :: OSI Approved :: MIT License', + + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.6', + ], + + keywords='nobilis', + + packages=find_packages('src', exclude=['contrib', 'docs', 'tests']), + package_dir={'': 'src'}, + + extras_require={ + 'test': ['coverage', 'pytest'], + }, +) diff --git a/src/nobilis/__init__.py b/src/nobilis/__init__.py new file mode 100644 diff --git a/src/nobilis/game_data.py b/src/nobilis/game_data.py new file mode 100644 --- /dev/null +++ b/src/nobilis/game_data.py @@ -0,0 +1,265 @@ +import collections + + +KeyType = collections.namedtuple('KeyType', 'strength weakness') + +POWER_KEY = KeyType('heart', 'shadow') +DECEIVER_KEY = KeyType('lie', 'truth') + +KeyTemplate = collections.namedtuple( + 'KeyTemplate', + 'type flower key_name names strength_hint weakness_bullets') + + +POWER_TEMPLATES = () +POWER_TEMPLATES += KeyTemplate( + POWER_KEY, 'Lotus Flower', 'Key of the Descending Angel', + ('My Power', 'My Nature'), + "how you enact meaningful change in people's lives", + ('glorious', 'set apart')), +POWER_TEMPLATES += KeyTemplate( + POWER_KEY, 'Vervain', 'Key of Something Powerful', + ('My Identity', 'Burdens'), 'who you are when you wield power', + ('bound by duty', 'set apart')), +POWER_TEMPLATES += KeyTemplate( + POWER_KEY, 'Acacia', 'Key of Something that Must Be Hidden', + ('A Dangerous Secret', '...That Hurts To Keep'), + 'what you do when dealing with the secret', + ('painful silence', 'set apart')), +POWER_TEMPLATES += KeyTemplate( + POWER_KEY, 'Clematis', 'Key of Something, Given Gifts', + ('I Have Witnessed It', 'Why I Doubt'), + 'who you are when you are witness to grace', ()), +POWER_TEMPLATES += KeyTemplate( + POWER_KEY, 'Aconite', 'Key of Rage', + # Not sure how best to represent this. + ("(.*'s )?(Rage|Hatred|Fury)", 'What Lets Me Oppose It'), + 'who you are in this drama', ()), +POWER_TEMPLATES += KeyTemplate( + POWER_KEY, 'Wild Oats', 'Key of Destiny Denied', + ('Hunger', 'Why It Feels Empty'), + 'who you are when indulging in your vice', ()), +POWER_TEMPLATES += KeyTemplate( + POWER_KEY, 'Star of Bethlehem', 'Key of Something, Changed Forever', + ('What it Made Me', 'How It Changed the World'), + "who you are when you're thinking about the mystery", + ("I can't let go", "I'm committed")), +POWER_TEMPLATES += KeyTemplate( + POWER_KEY, 'Gorse', 'Key of Something in Thrall', + ('Held in Thrall', 'Entangled with my Enemy'), + 'how you are held in thrall, and by what', + ("I can't let go", "I'm weak")), +POWER_TEMPLATES += KeyTemplate( + POWER_KEY, 'Honeysuckle', 'Key of Something, Finally, at Rest', + ('My New Life', 'My Old Life'), 'who you are and who you were', ()), +POWER_TEMPLATES += KeyTemplate( + POWER_KEY, 'Chamomile', 'Key of Something Romantic', + ('The Dream', 'The Cost'), 'who you are', ()), +POWER_TEMPLATES += KeyTemplate( + POWER_KEY, 'Wild Rose', 'Key of Something Different', + ('My Nature', 'My Struggles'), 'who you are compared to others', + ('self-doubt', 'trying to fit in')), +POWER_TEMPLATES += KeyTemplate( + POWER_KEY, 'Hollyhock', 'Key of Destiny in the Balance', + ('My Destiny', 'My Struggles'), 'what you are doing as you change', + ('self-doubt', 'changing hurts')), +POWER_TEMPLATES += KeyTemplate( + POWER_KEY, 'Mimulus', 'Key of Something Restless', + ('My Path', 'Why it Feels Empty'), 'who you are when you wander', + ('self-doubt', "the things I've left behind")), +POWER_TEMPLATES += KeyTemplate( + POWER_KEY, 'Water Lily', 'Key of Something Spiritual', + ('My Retreat', 'Burdens'), + 'who you are when you separate yourself from the world', + ('my duties as a Power',)), +POWER_TEMPLATES += KeyTemplate( + POWER_KEY, 'Oak', "Key of Something That Hasn't Changed", + ('My Identity', 'Crisis'), 'who you are', ()), +POWER_TEMPLATES += KeyTemplate( + POWER_KEY, 'Alyssym', 'Key of Destiny Fulfilled', + ('My Destiny', None), 'who you are, in terms of who you were', ()), + +DECEIVER_TEMPLATES = () +DECEIVER_TEMPLATES += KeyTemplate( + DECEIVER_KEY, 'Lotus Flower', 'Key of the Shepherd', 'I BRING MY FLOCK...', + 'what people get out of interacting with you', + ('I love them', 'I listen to them')), +DECEIVER_TEMPLATES += KeyTemplate( + DECEIVER_KEY, 'Vervain', 'Key of the Dragon', 'THE EXCRUCIAN WAR', + 'cool stuff that you could do or have done', + ('I love them', 'I am their enemy')), +DECEIVER_TEMPLATES += KeyTemplate( + DECEIVER_KEY, 'Acacia', 'Key of the Crossroads', 'WHY I FALTER', + 'your excuse for being kind to mortals and Powers', + ('I love them', "I've lost sight of what I'm fighting for")), +DECEIVER_TEMPLATES += KeyTemplate( + DECEIVER_KEY, 'Clematis', 'Key of the Angel', "I'M REACHING OUT TO YOU", + "who you pretend to be when you're reaching out to the Powers", + ()), +DECEIVER_TEMPLATES += KeyTemplate( + DECEIVER_KEY, 'Aconite', 'Key of the Nemesis', 'THE EXCRUCIAN WAR', + "who you are when ruining a Power's life", ()), +DECEIVER_TEMPLATES += KeyTemplate( + DECEIVER_KEY, 'Wild Oats', 'Key of the Dreamer', + 'WHO I AM, IN THIS YOUR DREAM', + 'the role you take on in this dream', ()), +DECEIVER_TEMPLATES += KeyTemplate( + DECEIVER_KEY, 'Star of Bethlehem', 'Key of Burdens', 'IT MARKED ME', + "what you do to show you're carrying something with you", + ("I can't let go", "It's always with me")), +DECEIVER_TEMPLATES += KeyTemplate( + DECEIVER_KEY, 'Gorse', 'Key of Promises', 'HELD IN THRALL', + "the situation you're trapped in", ("I can't let go", "It's killing me")), +DECEIVER_TEMPLATES += KeyTemplate( + DECEIVER_KEY, 'Periwinkle', 'Key of Legends', 'MY LEGEND', + 'an impressive thing you did', ()), +DECEIVER_TEMPLATES += KeyTemplate( + DECEIVER_KEY, 'Chamomile', 'Key of the Mask', + 'WHO I AM, IN THIS YOUR DREAM', 'who you are', ()), +DECEIVER_TEMPLATES += KeyTemplate( + DECEIVER_KEY, 'Widow-Rose', 'Key of the Alien', 'MY NATURE', + 'why you cannot fit in', + ("I'm an enemy of the world", 'You cannot keep me away')), +DECEIVER_TEMPLATES += KeyTemplate( + DECEIVER_KEY, 'Milkmay', 'Key Unformed', 'THAT WHICH STIRS', + "what form you're likely to take when play begins", + ("I'm an enemy of the world", 'I am truth preceding form')), +DECEIVER_TEMPLATES += KeyTemplate( + DECEIVER_KEY, 'Phoenix Posy', 'Self-Creating Key, the', + 'I HAVE MADE MYSELF...', 'how you fit into the world', + ("I'm an enemy of the world", 'from nothing')), +DECEIVER_TEMPLATES += KeyTemplate( + DECEIVER_KEY, 'Water Lily', 'Key of Something Spiritual', 'MY NATURE', + 'how your holiness expresses itself in the world', + ('Excrucian Deceiver',)), +DECEIVER_TEMPLATES += KeyTemplate( + DECEIVER_KEY, 'Bilberry', 'Key of the Fallen', 'WHAT I BELIEVE', + "what you've accepted", ()), +DECEIVER_TEMPLATES += KeyTemplate( + DECEIVER_KEY, 'Hemlock', 'Key of the Lie', 'WHO I AM, IN THIS YOUR DREAM', + "the story you're trying to tell (often part of another Key)", + ()), + + +GeneralTemplate = collections.namedtuple('GeneralTemplate', 'name indices') + + +FOUNDATION_TEMPLATES = () +FOUNDATION_TEMPLATES += GeneralTemplate( + 'Something Cool', (1, 2, 10, 15)), +FOUNDATION_TEMPLATES += GeneralTemplate( + 'In Love with Something', (4, 7, 9, 16)), +FOUNDATION_TEMPLATES += GeneralTemplate( + 'Epic, Inhuman, and Powerful', (5, 6, 12, 14)), +FOUNDATION_TEMPLATES += GeneralTemplate( + 'Just Plain Weird', (3, 8, 11, 13)), + +ESTATE_SOURCE_TEMPLATES = () +ESTATE_SOURCE_TEMPLATES += GeneralTemplate( + 'Light Side of Human Experience', (2, 6, 10, 12)), +ESTATE_SOURCE_TEMPLATES += GeneralTemplate( + 'Dark Side of Human Experience', (3, 5, 11, 15)), +ESTATE_SOURCE_TEMPLATES += GeneralTemplate( + 'Beautiful Side of the World', (4, 7, 14, 16)), +ESTATE_SOURCE_TEMPLATES += GeneralTemplate( + 'Painful Side of the World', (1, 8, 9, 13)), + +ESTATE_NATURE_TEMPLATES = () +ESTATE_NATURE_TEMPLATES += GeneralTemplate( + 'Something You Can Point To', (8, 9, 13, 14, 15)), +ESTATE_NATURE_TEMPLATES += GeneralTemplate( + 'Something You Live', (3, 5, 6, 4, 11, 12)), +ESTATE_NATURE_TEMPLATES += GeneralTemplate( + 'Something You Can Describe', (1, 2, 7, 10, 16)), + +ORIGIN_TEMPLATES = () +ORIGIN_TEMPLATES += GeneralTemplate('Troubled Life', (2, 5, 6, 11)), +ORIGIN_TEMPLATES += GeneralTemplate('Humble Life', (8, 9, 14, 15)), +ORIGIN_TEMPLATES += GeneralTemplate('Blessed Life', (1, 4, 12, 16)), +ORIGIN_TEMPLATES += GeneralTemplate('Extraordinary Life', (3, 7, 10, 13)), + +TROUBLED_TEMPLATES = () +TROUBLED_TEMPLATES += GeneralTemplate( + "You're Still in Trouble!", (3, 5, 8, 13)), +TROUBLED_TEMPLATES += GeneralTemplate( + 'Some Scars Remain', (7, 11, 12, 14)), +TROUBLED_TEMPLATES += GeneralTemplate( + "It's All Happening Again!", (6, 10, 15, 16)), +TROUBLED_TEMPLATES += GeneralTemplate( + 'Trouble Inspired Me', (1, 2, 4, 9)), + +HUMBLE_TEMPLATES = () +HUMBLE_TEMPLATES += GeneralTemplate( + 'Love for the Ordinary...', (6, 7, 15, 16)), +HUMBLE_TEMPLATES += GeneralTemplate( + 'Alienation...', (3, 5, 11, 14)), +HUMBLE_TEMPLATES += GeneralTemplate( + 'Transformation...', (4, 8, 12, 13)), +HUMBLE_TEMPLATES += GeneralTemplate( + 'Freedom!', (1, 2, 9, 10)), + +BLESSED_TEMPLATES = () +BLESSED_TEMPLATES += GeneralTemplate('Reverence in Purpose', (2, 4, 10, 13)), +BLESSED_TEMPLATES += GeneralTemplate('Community', (1, 3, 8, 15)), +BLESSED_TEMPLATES += GeneralTemplate('Your Way of Life', (7, 9, 14, 16)), +BLESSED_TEMPLATES += GeneralTemplate('Anger', (5, 6, 9, 12)), + +NORMAL_CONTACT_TEMPLATES = () +NORMAL_CONTACT_TEMPLATES += GeneralTemplate( + 'Organization', (1,)), +NORMAL_CONTACT_TEMPLATES += GeneralTemplate( + 'Followers', (2,)), +NORMAL_CONTACT_TEMPLATES += GeneralTemplate( + 'Excrucian', (3,)), +NORMAL_CONTACT_TEMPLATES += GeneralTemplate( + 'Inspirational Friend or Lover', (4,)), +NORMAL_CONTACT_TEMPLATES += GeneralTemplate( + 'Nemesis', (5,)), +NORMAL_CONTACT_TEMPLATES += GeneralTemplate( + 'Corrupting Influence', (6,)), +NORMAL_CONTACT_TEMPLATES += GeneralTemplate( + 'Ghost', (7,)), +NORMAL_CONTACT_TEMPLATES += GeneralTemplate( + 'Cammora', (8,)), +NORMAL_CONTACT_TEMPLATES += GeneralTemplate( + 'Legacy', (9,)), +NORMAL_CONTACT_TEMPLATES += GeneralTemplate( + 'True Love', (10,)), +NORMAL_CONTACT_TEMPLATES += GeneralTemplate( + 'Alien(s)', (11,)), +NORMAL_CONTACT_TEMPLATES += GeneralTemplate( + 'Manufactured Army', (12,)), +NORMAL_CONTACT_TEMPLATES += GeneralTemplate( + 'Faraway or Troubled Love', (13,)), +NORMAL_CONTACT_TEMPLATES += GeneralTemplate( + 'Ward', (14,)), +NORMAL_CONTACT_TEMPLATES += GeneralTemplate( + 'Moral Friends and Family', (15,)), +NORMAL_CONTACT_TEMPLATES += GeneralTemplate( + 'Disciples', (16,)), + +SPECIAL_CONTACT_TEMPLATES = () +SPECIAL_CONTACT_TEMPLATES += GeneralTemplate('Noble Friend or Enemy', ()), +SPECIAL_CONTACT_TEMPLATES += GeneralTemplate('Cleave of the Botanists', ()), +SPECIAL_CONTACT_TEMPLATES += GeneralTemplate('Mystery Cult', ()), + + +AFFLIATION_TEMPLATES = () +AFFLIATION_TEMPLATES += GeneralTemplate( + '(The Song of) Heaven', (1, 2, 3)), +AFFLIATION_TEMPLATES += GeneralTemplate( + '(The Song of) Hell', (4, 5, 6)), +AFFLIATION_TEMPLATES += GeneralTemplate( + '(The Song of) The Light', (7, 8)), +AFFLIATION_TEMPLATES += GeneralTemplate( + '(The Song of) The Dark', (9, 10)), +AFFLIATION_TEMPLATES += GeneralTemplate( + '(The Song of) The Wild', (11, 12, 13)), +AFFLIATION_TEMPLATES += GeneralTemplate( + 'An Independent Song', (14, 15, 16)), + + +BACKGROUND_TEMPLATES = () +BACKGROUND_TEMPLATES += GeneralTemplate('Animal', (6, 8, 14, 15)), +BACKGROUND_TEMPLATES += GeneralTemplate('Strange', (4, 11, 12, 16)), +BACKGROUND_TEMPLATES += GeneralTemplate('"Sort of" Human', (1, 7, 10, 13)), diff --git a/src/nobilis/lifepath.py b/src/nobilis/lifepath.py new file mode 100644 --- /dev/null +++ b/src/nobilis/lifepath.py @@ -0,0 +1,62 @@ +import collections + +from . import visitor + + +nt = collections.namedtuple + +Business = visitor.make_visitor() + + +def key(item): + return type(item), item + + +class Parentable: + + __slots__ = () + + +P = Parentable + + +@Business.mark +class Choice(P, nt('Choice', 'template')): + + __slots__ = () + + +@Business.mark +class ChoiceDisplay(P, nt('ChoiceDisplay', 'choice')): + + __slots__ = () + + +@Business.mark +class ChoiceTaken(P, nt('ChoiceTaken', 'choice')): + + __slots__ = () + + +@Business.mark +class ChoiceListed(P, nt('ChoiceListed', 'choice')): + + __slots__ = () + + +@Business.mark +class ChoiceList(P, nt('ChoiceList', 'choices initial_min initial_max')): + + __slots__ = () + + +@Business.mark +class DisplayList(P, nt('DisplayList', 'factory choices_ sections_')): + + __slots__ = () + + +@Business.mark +class Section(P, nt('Section', 'display_list name')): + + __slots__ = () diff --git a/src/nobilis/visitor.py b/src/nobilis/visitor.py new file mode 100644 --- /dev/null +++ b/src/nobilis/visitor.py @@ -0,0 +1,88 @@ +"""This module provides an abstract class for visitors.""" + +import functools + +SENTINEL = object() + + +def visitor_func(self, node, function, old_visitor): + def next_visitor(arg=None): + return function(self, node, old_visitor) + return next_visitor + + +def identity(arg=None): + return arg + + +def make_visitor(default=None): + """Return a base Visitor class.""" + class Visitor: + + __classes = set() + + def __new__(cls, *args, **kwargs): + if cls is Visitor: + raise TypeError + return super().__new__(cls, *args, **kwargs) + + def __init_subclass__(cls, **kwargs): + super().__init_subclass__(**kwargs) + cls.__visitors = {} + + if default is not None: + default_visit = default + else: + def default_visit(self, node): + raise NotImplementedError( + f'No implementation for {node} on {self}.') + + def __get_visitor(self, node_class): + my_mro = type(self).mro() + visitor_index = my_mro.index(Visitor) + my_mro = my_mro[:visitor_index] + visitor_mro = [] + for cls in node_class.mro(): + for visitor_class in my_mro: + try: + visitor_dict = visitor_class.__visitors + except AttributeError: + pass + else: + visitor = visitor_dict.get(cls, SENTINEL) + if visitor is not SENTINEL: + visitor_mro.append(visitor) + return visitor_mro + + def visit(self, node): + visitors = self.__get_visitor(type(node)) + if visitors: + visitor = identity + while visitors: + visitor = visitor_func(self, node, visitors.pop(), visitor) + return visitor() + else: + return self.default_visit(node) + + @classmethod + def bind(cls, node_class, function=None): + if cls is Visitor: + raise NotImplementedError + if node_class not in cls.__classes: + raise ValueError + if function is None: + return functools.partial(cls.bind, node_class) + cls.__visitors[node_class] = function + return cls + + @classmethod + def mark(cls, node_class): + cls.__classes.add(node_class) + return node_class + + @classmethod + def undefined(cls): + if cls is Visitor: + raise ValueError + return cls.__classes.difference(cls.__visitors) + return Visitor diff --git a/tests/nobilis/__init__.py b/tests/nobilis/__init__.py new file mode 100644 diff --git a/tests/nobilis/conftest.py b/tests/nobilis/conftest.py new file mode 100644 --- /dev/null +++ b/tests/nobilis/conftest.py @@ -0,0 +1,19 @@ +import pytest + + +@pytest.fixture +def visitor(): + import nobilis.visitor + return nobilis.visitor + + +@pytest.fixture +def game_data(): + import nobilis.game_data + return nobilis.game_data + + +@pytest.fixture +def lifepath(): + import nobilis.lifepath + return nobilis.lifepath diff --git a/tests/nobilis/test_game_data.py b/tests/nobilis/test_game_data.py new file mode 100644 --- /dev/null +++ b/tests/nobilis/test_game_data.py @@ -0,0 +1,2 @@ +def test_import(game_data): + assert game_data diff --git a/tests/nobilis/test_lifepath.py b/tests/nobilis/test_lifepath.py new file mode 100644 --- /dev/null +++ b/tests/nobilis/test_lifepath.py @@ -0,0 +1,7 @@ +def test_import(lifepath): + assert lifepath + + +def test_key(lifepath): + assert lifepath.key(lifepath.Choice(None)) != lifepath.key( + lifepath.ChoiceDisplay(None)) diff --git a/tests/nobilis/test_visitor.py b/tests/nobilis/test_visitor.py new file mode 100644 --- /dev/null +++ b/tests/nobilis/test_visitor.py @@ -0,0 +1,149 @@ +import pytest + + +def test_import(visitor): + assert visitor + + +def test_visitor(visitor): + assert visitor.make_visitor() + + +def test_mark(visitor): + Visitor = visitor.make_visitor() + + @Visitor.mark + class Test: + pass + + class Test2: + pass + + Test3 = Visitor.mark(Test2) + assert Test2 is Test3 + + +def test_cannot_instantiate_base_class(visitor): + Visitor = visitor.make_visitor() + + with pytest.raises(TypeError): + print(Visitor()) + + class Subclass(Visitor): + pass + + assert Subclass() + + +def test_bind(visitor): + Visitor = visitor.make_visitor() + + @Visitor.mark + class Test: + pass + + class Subclass(Visitor): + pass + + with pytest.raises(ValueError): + print(Visitor.undefined()) + + assert Subclass.undefined() == {Test} + + with pytest.raises(NotImplementedError): + Visitor.bind(Test) + + with pytest.raises(NotImplementedError): + Subclass().visit(Test()) + + @Subclass.bind(Test) + def Subclass(self, node, next_visitor): + return node + + assert Subclass.undefined() == set() + + test = Test() + assert Subclass().visit(test) is test + + +def test_provide_default(visitor): + @visitor.make_visitor + def Visitor(self, node): + return node + + @Visitor.mark + class Test: + pass + + class Subclass(Visitor): + pass + + test = Test() + assert Subclass().visit(test) is test + + +def test_cannot_add_unmarked(visitor): + Visitor = visitor.make_visitor() + + class Subclass(Visitor): + pass + + with pytest.raises(ValueError): + Subclass.bind(object) + + Visitor.mark(object) + + assert Subclass.bind(object) + + +def test_multiple_inheritance(visitor): + Visitor = visitor.make_visitor() + + class Klass: + pass + + class Subclass(Klass, Visitor): + pass + + @Visitor.mark + class Test: + pass + + with pytest.raises(NotImplementedError): + Subclass().visit(Test()) + + +def test_next_visitor(visitor): + Visitor = visitor.make_visitor() + + @Visitor.mark + class Test1: + pass + + @Visitor.mark + class Test2(Test1): + pass + + class Visitor1(Visitor): + pass + + class Visitor2(Visitor1): + pass + + @Visitor2.bind(Test2) + def Visitor2(self, node, next_visitor): + return 'a' + next_visitor('f') + + @Visitor1.bind(Test2) + def Visitor1(self, node, next_visitor): + return 'b' + next_visitor('f') + + @Visitor2.bind(Test1) + def Visitor2(self, node, next_visitor): + return 'c' + next_visitor('f') + + @Visitor1.bind(Test1) + def Visitor1(self, node, next_visitor): + return 'd' + next_visitor('e') + + assert Visitor2().visit(Test2()) == 'abcde' diff --git a/tests/test_nobilis.py b/tests/test_nobilis.py new file mode 100644 --- /dev/null +++ b/tests/test_nobilis.py @@ -0,0 +1,3 @@ +def test_import_nobilis(): + import nobilis + assert nobilis diff --git a/tox.ini b/tox.ini new file mode 100644 --- /dev/null +++ b/tox.ini @@ -0,0 +1,17 @@ +# Tox (http://tox.testrun.org/) is a tool for running tests +# in multiple virtualenvs. This configuration file will run the +# test suite on all supported python versions. To use it, "pip install tox" +# and then run "tox" from this directory. + +[tox] +envlist = py36 + +[testenv] +commands = + {envpython} -m divide_and_cover run -m pytest --divide-and-cover + {envpython} -m divide_and_cover html -d htmlcov/{envname} +deps = + pytest-divide-and-cover + divide-and-cover + pytest + coverage