mwchase

LC:Nobilis Update 4: diff

May 21st, 2017
1,420
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Diff 24.00 KB | None | 0 0
  1. diff --git a/.coveragerc b/.coveragerc
  2. new file mode 100644
  3. --- /dev/null
  4. +++ b/.coveragerc
  5. @@ -0,0 +1,2 @@
  6. +[run]
  7. +branch = True
  8. diff --git a/.hgignore b/.hgignore
  9. new file mode 100644
  10. --- /dev/null
  11. +++ b/.hgignore
  12. @@ -0,0 +1,96 @@
  13. +syntax: glob
  14. +# Byte-compiled / optimized / DLL files
  15. +__pycache__/
  16. +*.py[cod]
  17. +
  18. +# C extensions
  19. +*.so
  20. +
  21. +# Distribution / packaging
  22. +.Python
  23. +env/
  24. +build/
  25. +develop-eggs/
  26. +dist/
  27. +downloads/
  28. +eggs/
  29. +lib/
  30. +lib64/
  31. +parts/
  32. +sdist/
  33. +var/
  34. +*.egg-info/
  35. +.installed.cfg
  36. +*.egg
  37. +
  38. +# PyInstaller
  39. +#  Usually these files are written by a python script from a template
  40. +#  before PyInstaller builds the exe, so as to inject date/other infos into it.
  41. +*.manifest
  42. +*.spec
  43. +
  44. +# Installer logs
  45. +pip-log.txt
  46. +pip-delete-this-directory.txt
  47. +
  48. +# Unit test / coverage reports
  49. +htmlcov/
  50. +.tox/
  51. +.coverage
  52. +.cache
  53. +nosetests.xml
  54. +coverage.xml
  55. +
  56. +# Translations
  57. +*.mo
  58. +*.pot
  59. +
  60. +# Django stuff:
  61. +*.log
  62. +
  63. +# Sphinx documentation
  64. +docs/_build/
  65. +
  66. +# PyBuilder
  67. +target/
  68. +
  69. +
  70. +# cache files for sublime text
  71. +*.tmlanguage.cache
  72. +*.tmPreferences.cache
  73. +*.stTheme.cache
  74. +
  75. +# workspace files are user-specific
  76. +*.sublime-workspace
  77. +
  78. +# project files should be checked into the repository, unless a significant
  79. +# proportion of contributors will probably not be using SublimeText
  80. +# *.sublime-project
  81. +
  82. +# sftp configuration file
  83. +sftp-config.json
  84. +
  85. +
  86. +.DS_Store
  87. +.AppleDouble
  88. +.LSOverride
  89. +
  90. +# Icon must end with two \r
  91. +Icon
  92. +
  93. +
  94. +# Thumbnails
  95. +._*
  96. +
  97. +# Files that might appear on external disk
  98. +.Spotlight-V100
  99. +.Trashes
  100. +
  101. +# Directories potentially created on remote AFP share
  102. +.AppleDB
  103. +.AppleDesktop
  104. +Network Trash Folder
  105. +Temporary Items
  106. +.apdisk
  107. +
  108. +
  109. diff --git a/README.rst b/README.rst
  110. new file mode 100644
  111. diff --git a/data.py b/prototype/data.py
  112. rename from data.py
  113. rename to prototype/data.py
  114. diff --git a/nobilis.py b/prototype/nobilis.py
  115. rename from nobilis.py
  116. rename to prototype/nobilis.py
  117. diff --git a/setup.py b/setup.py
  118. new file mode 100644
  119. --- /dev/null
  120. +++ b/setup.py
  121. @@ -0,0 +1,47 @@
  122. +"""A setuptools based setup module."""
  123. +
  124. +# Always prefer setuptools over distutils
  125. +from setuptools import setup, find_packages
  126. +# To use a consistent encoding
  127. +from codecs import open as c_open
  128. +from os import path
  129. +
  130. +HERE = path.abspath(path.dirname(__file__))
  131. +
  132. +# Get the long description from the README file
  133. +with c_open(path.join(HERE, 'README.rst'), encoding='utf-8') as f:
  134. +    LONG_DESCRIPTION = f.read()
  135. +
  136. +setup(
  137. +    name='nobilis',
  138. +
  139. +    version='0.1.0',
  140. +
  141. +    description='Nobilis character creator',
  142. +    long_description=LONG_DESCRIPTION,
  143. +
  144. +    # url='https://github.com/mwchase/class-namespaces',
  145. +
  146. +    author='Max Woerner Chase',
  147. +    author_email='[email protected]',
  148. +
  149. +    license='MIT',
  150. +
  151. +    classifiers=[
  152. +        'Development Status :: 3 - Alpha',
  153. +
  154. +        'License :: OSI Approved :: MIT License',
  155. +
  156. +        'Programming Language :: Python :: 3',
  157. +        'Programming Language :: Python :: 3.6',
  158. +    ],
  159. +
  160. +    keywords='nobilis',
  161. +
  162. +    packages=find_packages('src', exclude=['contrib', 'docs', 'tests']),
  163. +    package_dir={'': 'src'},
  164. +
  165. +    extras_require={
  166. +        'test': ['coverage', 'pytest'],
  167. +    },
  168. +)
  169. diff --git a/src/nobilis/__init__.py b/src/nobilis/__init__.py
  170. new file mode 100644
  171. diff --git a/src/nobilis/game_data.py b/src/nobilis/game_data.py
  172. new file mode 100644
  173. --- /dev/null
  174. +++ b/src/nobilis/game_data.py
  175. @@ -0,0 +1,265 @@
  176. +import collections
  177. +
  178. +
  179. +KeyType = collections.namedtuple('KeyType', 'strength weakness')
  180. +
  181. +POWER_KEY = KeyType('heart', 'shadow')
  182. +DECEIVER_KEY = KeyType('lie', 'truth')
  183. +
  184. +KeyTemplate = collections.namedtuple(
  185. +    'KeyTemplate',
  186. +    'type flower key_name names strength_hint weakness_bullets')
  187. +
  188. +
  189. +POWER_TEMPLATES = ()
  190. +POWER_TEMPLATES += KeyTemplate(
  191. +    POWER_KEY, 'Lotus Flower', 'Key of the Descending Angel',
  192. +    ('My Power', 'My Nature'),
  193. +    "how you enact meaningful change in people's lives",
  194. +    ('glorious', 'set apart')),
  195. +POWER_TEMPLATES += KeyTemplate(
  196. +    POWER_KEY, 'Vervain', 'Key of Something Powerful',
  197. +    ('My Identity', 'Burdens'), 'who you are when you wield power',
  198. +    ('bound by duty', 'set apart')),
  199. +POWER_TEMPLATES += KeyTemplate(
  200. +    POWER_KEY, 'Acacia', 'Key of Something that Must Be Hidden',
  201. +    ('A Dangerous Secret', '...That Hurts To Keep'),
  202. +    'what you do when dealing with the secret',
  203. +    ('painful silence', 'set apart')),
  204. +POWER_TEMPLATES += KeyTemplate(
  205. +    POWER_KEY, 'Clematis', 'Key of Something, Given Gifts',
  206. +    ('I Have Witnessed It', 'Why I Doubt'),
  207. +    'who you are when you are witness to grace', ()),
  208. +POWER_TEMPLATES += KeyTemplate(
  209. +    POWER_KEY, 'Aconite', 'Key of Rage',
  210. +    # Not sure how best to represent this.
  211. +    ("(.*'s )?(Rage|Hatred|Fury)", 'What Lets Me Oppose It'),
  212. +    'who you are in this drama', ()),
  213. +POWER_TEMPLATES += KeyTemplate(
  214. +    POWER_KEY, 'Wild Oats', 'Key of Destiny Denied',
  215. +    ('Hunger', 'Why It Feels Empty'),
  216. +    'who you are when indulging in your vice', ()),
  217. +POWER_TEMPLATES += KeyTemplate(
  218. +    POWER_KEY, 'Star of Bethlehem', 'Key of Something, Changed Forever',
  219. +    ('What it Made Me', 'How It Changed the World'),
  220. +    "who you are when you're thinking about the mystery",
  221. +    ("I can't let go", "I'm committed")),
  222. +POWER_TEMPLATES += KeyTemplate(
  223. +    POWER_KEY, 'Gorse', 'Key of Something in Thrall',
  224. +    ('Held in Thrall', 'Entangled with my Enemy'),
  225. +    'how you are held in thrall, and by what',
  226. +    ("I can't let go", "I'm weak")),
  227. +POWER_TEMPLATES += KeyTemplate(
  228. +    POWER_KEY, 'Honeysuckle', 'Key of Something, Finally, at Rest',
  229. +    ('My New Life', 'My Old Life'), 'who you are and who you were', ()),
  230. +POWER_TEMPLATES += KeyTemplate(
  231. +    POWER_KEY, 'Chamomile', 'Key of Something Romantic',
  232. +    ('The Dream', 'The Cost'), 'who you are', ()),
  233. +POWER_TEMPLATES += KeyTemplate(
  234. +    POWER_KEY, 'Wild Rose', 'Key of Something Different',
  235. +    ('My Nature', 'My Struggles'), 'who you are compared to others',
  236. +    ('self-doubt', 'trying to fit in')),
  237. +POWER_TEMPLATES += KeyTemplate(
  238. +    POWER_KEY, 'Hollyhock', 'Key of Destiny in the Balance',
  239. +    ('My Destiny', 'My Struggles'), 'what you are doing as you change',
  240. +    ('self-doubt', 'changing hurts')),
  241. +POWER_TEMPLATES += KeyTemplate(
  242. +    POWER_KEY, 'Mimulus', 'Key of Something Restless',
  243. +    ('My Path', 'Why it Feels Empty'), 'who you are when you wander',
  244. +    ('self-doubt', "the things I've left behind")),
  245. +POWER_TEMPLATES += KeyTemplate(
  246. +    POWER_KEY, 'Water Lily', 'Key of Something Spiritual',
  247. +    ('My Retreat', 'Burdens'),
  248. +    'who you are when you separate yourself from the world',
  249. +    ('my duties as a Power',)),
  250. +POWER_TEMPLATES += KeyTemplate(
  251. +    POWER_KEY, 'Oak', "Key of Something That Hasn't Changed",
  252. +    ('My Identity', 'Crisis'), 'who you are', ()),
  253. +POWER_TEMPLATES += KeyTemplate(
  254. +    POWER_KEY, 'Alyssym', 'Key of Destiny Fulfilled',
  255. +    ('My Destiny', None), 'who you are, in terms of who you were', ()),
  256. +
  257. +DECEIVER_TEMPLATES = ()
  258. +DECEIVER_TEMPLATES += KeyTemplate(
  259. +    DECEIVER_KEY, 'Lotus Flower', 'Key of the Shepherd', 'I BRING MY FLOCK...',
  260. +    'what people get out of interacting with you',
  261. +    ('I love them', 'I listen to them')),
  262. +DECEIVER_TEMPLATES += KeyTemplate(
  263. +    DECEIVER_KEY, 'Vervain', 'Key of the Dragon', 'THE EXCRUCIAN WAR',
  264. +    'cool stuff that you could do or have done',
  265. +    ('I love them', 'I am their enemy')),
  266. +DECEIVER_TEMPLATES += KeyTemplate(
  267. +    DECEIVER_KEY, 'Acacia', 'Key of the Crossroads', 'WHY I FALTER',
  268. +    'your excuse for being kind to mortals and Powers',
  269. +    ('I love them', "I've lost sight of what I'm fighting for")),
  270. +DECEIVER_TEMPLATES += KeyTemplate(
  271. +    DECEIVER_KEY, 'Clematis', 'Key of the Angel', "I'M REACHING OUT TO YOU",
  272. +    "who you pretend to be when you're reaching out to the Powers",
  273. +    ()),
  274. +DECEIVER_TEMPLATES += KeyTemplate(
  275. +    DECEIVER_KEY, 'Aconite', 'Key of the Nemesis', 'THE EXCRUCIAN WAR',
  276. +    "who you are when ruining a Power's life", ()),
  277. +DECEIVER_TEMPLATES += KeyTemplate(
  278. +    DECEIVER_KEY, 'Wild Oats', 'Key of the Dreamer',
  279. +    'WHO I AM, IN THIS YOUR DREAM',
  280. +    'the role you take on in this dream', ()),
  281. +DECEIVER_TEMPLATES += KeyTemplate(
  282. +    DECEIVER_KEY, 'Star of Bethlehem', 'Key of Burdens', 'IT MARKED ME',
  283. +    "what you do to show you're carrying something with you",
  284. +    ("I can't let go", "It's always with me")),
  285. +DECEIVER_TEMPLATES += KeyTemplate(
  286. +    DECEIVER_KEY, 'Gorse', 'Key of Promises', 'HELD IN THRALL',
  287. +    "the situation you're trapped in", ("I can't let go", "It's killing me")),
  288. +DECEIVER_TEMPLATES += KeyTemplate(
  289. +    DECEIVER_KEY, 'Periwinkle', 'Key of Legends', 'MY LEGEND',
  290. +    'an impressive thing you did', ()),
  291. +DECEIVER_TEMPLATES += KeyTemplate(
  292. +    DECEIVER_KEY, 'Chamomile', 'Key of the Mask',
  293. +    'WHO I AM, IN THIS YOUR DREAM', 'who you are', ()),
  294. +DECEIVER_TEMPLATES += KeyTemplate(
  295. +    DECEIVER_KEY, 'Widow-Rose', 'Key of the Alien', 'MY NATURE',
  296. +    'why you cannot fit in',
  297. +    ("I'm an enemy of the world", 'You cannot keep me away')),
  298. +DECEIVER_TEMPLATES += KeyTemplate(
  299. +    DECEIVER_KEY, 'Milkmay', 'Key Unformed', 'THAT WHICH STIRS',
  300. +    "what form you're likely to take when play begins",
  301. +    ("I'm an enemy of the world", 'I am truth preceding form')),
  302. +DECEIVER_TEMPLATES += KeyTemplate(
  303. +    DECEIVER_KEY, 'Phoenix Posy', 'Self-Creating Key, the',
  304. +    'I HAVE MADE MYSELF...', 'how you fit into the world',
  305. +    ("I'm an enemy of the world", 'from nothing')),
  306. +DECEIVER_TEMPLATES += KeyTemplate(
  307. +    DECEIVER_KEY, 'Water Lily', 'Key of Something Spiritual', 'MY NATURE',
  308. +    'how your holiness expresses itself in the world',
  309. +    ('Excrucian Deceiver',)),
  310. +DECEIVER_TEMPLATES += KeyTemplate(
  311. +    DECEIVER_KEY, 'Bilberry', 'Key of the Fallen', 'WHAT I BELIEVE',
  312. +    "what you've accepted", ()),
  313. +DECEIVER_TEMPLATES += KeyTemplate(
  314. +    DECEIVER_KEY, 'Hemlock', 'Key of the Lie', 'WHO I AM, IN THIS YOUR DREAM',
  315. +    "the story you're trying to tell (often part of another Key)",
  316. +    ()),
  317. +
  318. +
  319. +GeneralTemplate = collections.namedtuple('GeneralTemplate', 'name indices')
  320. +
  321. +
  322. +FOUNDATION_TEMPLATES = ()
  323. +FOUNDATION_TEMPLATES += GeneralTemplate(
  324. +    'Something Cool', (1, 2, 10, 15)),
  325. +FOUNDATION_TEMPLATES += GeneralTemplate(
  326. +    'In Love with Something', (4, 7, 9, 16)),
  327. +FOUNDATION_TEMPLATES += GeneralTemplate(
  328. +    'Epic, Inhuman, and Powerful', (5, 6, 12, 14)),
  329. +FOUNDATION_TEMPLATES += GeneralTemplate(
  330. +    'Just Plain Weird', (3, 8, 11, 13)),
  331. +
  332. +ESTATE_SOURCE_TEMPLATES = ()
  333. +ESTATE_SOURCE_TEMPLATES += GeneralTemplate(
  334. +    'Light Side of Human Experience', (2, 6, 10, 12)),
  335. +ESTATE_SOURCE_TEMPLATES += GeneralTemplate(
  336. +    'Dark Side of Human Experience', (3, 5, 11, 15)),
  337. +ESTATE_SOURCE_TEMPLATES += GeneralTemplate(
  338. +    'Beautiful Side of the World', (4, 7, 14, 16)),
  339. +ESTATE_SOURCE_TEMPLATES += GeneralTemplate(
  340. +    'Painful Side of the World', (1, 8, 9, 13)),
  341. +
  342. +ESTATE_NATURE_TEMPLATES = ()
  343. +ESTATE_NATURE_TEMPLATES += GeneralTemplate(
  344. +    'Something You Can Point To', (8, 9, 13, 14, 15)),
  345. +ESTATE_NATURE_TEMPLATES += GeneralTemplate(
  346. +    'Something You Live', (3, 5, 6, 4, 11, 12)),
  347. +ESTATE_NATURE_TEMPLATES += GeneralTemplate(
  348. +    'Something You Can Describe', (1, 2, 7, 10, 16)),
  349. +
  350. +ORIGIN_TEMPLATES = ()
  351. +ORIGIN_TEMPLATES += GeneralTemplate('Troubled Life', (2, 5, 6, 11)),
  352. +ORIGIN_TEMPLATES += GeneralTemplate('Humble Life', (8, 9, 14, 15)),
  353. +ORIGIN_TEMPLATES += GeneralTemplate('Blessed Life', (1, 4, 12, 16)),
  354. +ORIGIN_TEMPLATES += GeneralTemplate('Extraordinary Life', (3, 7, 10, 13)),
  355. +
  356. +TROUBLED_TEMPLATES = ()
  357. +TROUBLED_TEMPLATES += GeneralTemplate(
  358. +    "You're Still in Trouble!", (3, 5, 8, 13)),
  359. +TROUBLED_TEMPLATES += GeneralTemplate(
  360. +    'Some Scars Remain', (7, 11, 12, 14)),
  361. +TROUBLED_TEMPLATES += GeneralTemplate(
  362. +    "It's All Happening Again!", (6, 10, 15, 16)),
  363. +TROUBLED_TEMPLATES += GeneralTemplate(
  364. +    'Trouble Inspired Me', (1, 2, 4, 9)),
  365. +
  366. +HUMBLE_TEMPLATES = ()
  367. +HUMBLE_TEMPLATES += GeneralTemplate(
  368. +    'Love for the Ordinary...', (6, 7, 15, 16)),
  369. +HUMBLE_TEMPLATES += GeneralTemplate(
  370. +    'Alienation...', (3, 5, 11, 14)),
  371. +HUMBLE_TEMPLATES += GeneralTemplate(
  372. +    'Transformation...', (4, 8, 12, 13)),
  373. +HUMBLE_TEMPLATES += GeneralTemplate(
  374. +    'Freedom!', (1, 2, 9, 10)),
  375. +
  376. +BLESSED_TEMPLATES = ()
  377. +BLESSED_TEMPLATES += GeneralTemplate('Reverence in Purpose', (2, 4, 10, 13)),
  378. +BLESSED_TEMPLATES += GeneralTemplate('Community', (1, 3, 8, 15)),
  379. +BLESSED_TEMPLATES += GeneralTemplate('Your Way of Life', (7, 9, 14, 16)),
  380. +BLESSED_TEMPLATES += GeneralTemplate('Anger', (5, 6, 9, 12)),
  381. +
  382. +NORMAL_CONTACT_TEMPLATES = ()
  383. +NORMAL_CONTACT_TEMPLATES += GeneralTemplate(
  384. +    'Organization', (1,)),
  385. +NORMAL_CONTACT_TEMPLATES += GeneralTemplate(
  386. +    'Followers', (2,)),
  387. +NORMAL_CONTACT_TEMPLATES += GeneralTemplate(
  388. +    'Excrucian', (3,)),
  389. +NORMAL_CONTACT_TEMPLATES += GeneralTemplate(
  390. +    'Inspirational Friend or Lover', (4,)),
  391. +NORMAL_CONTACT_TEMPLATES += GeneralTemplate(
  392. +    'Nemesis', (5,)),
  393. +NORMAL_CONTACT_TEMPLATES += GeneralTemplate(
  394. +    'Corrupting Influence', (6,)),
  395. +NORMAL_CONTACT_TEMPLATES += GeneralTemplate(
  396. +    'Ghost', (7,)),
  397. +NORMAL_CONTACT_TEMPLATES += GeneralTemplate(
  398. +    'Cammora', (8,)),
  399. +NORMAL_CONTACT_TEMPLATES += GeneralTemplate(
  400. +    'Legacy', (9,)),
  401. +NORMAL_CONTACT_TEMPLATES += GeneralTemplate(
  402. +    'True Love', (10,)),
  403. +NORMAL_CONTACT_TEMPLATES += GeneralTemplate(
  404. +    'Alien(s)', (11,)),
  405. +NORMAL_CONTACT_TEMPLATES += GeneralTemplate(
  406. +    'Manufactured Army', (12,)),
  407. +NORMAL_CONTACT_TEMPLATES += GeneralTemplate(
  408. +    'Faraway or Troubled Love', (13,)),
  409. +NORMAL_CONTACT_TEMPLATES += GeneralTemplate(
  410. +    'Ward', (14,)),
  411. +NORMAL_CONTACT_TEMPLATES += GeneralTemplate(
  412. +    'Moral Friends and Family', (15,)),
  413. +NORMAL_CONTACT_TEMPLATES += GeneralTemplate(
  414. +    'Disciples', (16,)),
  415. +
  416. +SPECIAL_CONTACT_TEMPLATES = ()
  417. +SPECIAL_CONTACT_TEMPLATES += GeneralTemplate('Noble Friend or Enemy', ()),
  418. +SPECIAL_CONTACT_TEMPLATES += GeneralTemplate('Cleave of the Botanists', ()),
  419. +SPECIAL_CONTACT_TEMPLATES += GeneralTemplate('Mystery Cult', ()),
  420. +
  421. +
  422. +AFFLIATION_TEMPLATES = ()
  423. +AFFLIATION_TEMPLATES += GeneralTemplate(
  424. +    '(The Song of) Heaven', (1, 2, 3)),
  425. +AFFLIATION_TEMPLATES += GeneralTemplate(
  426. +    '(The Song of) Hell', (4, 5, 6)),
  427. +AFFLIATION_TEMPLATES += GeneralTemplate(
  428. +    '(The Song of) The Light', (7, 8)),
  429. +AFFLIATION_TEMPLATES += GeneralTemplate(
  430. +    '(The Song of) The Dark', (9, 10)),
  431. +AFFLIATION_TEMPLATES += GeneralTemplate(
  432. +    '(The Song of) The Wild', (11, 12, 13)),
  433. +AFFLIATION_TEMPLATES += GeneralTemplate(
  434. +    'An Independent Song', (14, 15, 16)),
  435. +
  436. +
  437. +BACKGROUND_TEMPLATES = ()
  438. +BACKGROUND_TEMPLATES += GeneralTemplate('Animal', (6, 8, 14, 15)),
  439. +BACKGROUND_TEMPLATES += GeneralTemplate('Strange', (4, 11, 12, 16)),
  440. +BACKGROUND_TEMPLATES += GeneralTemplate('"Sort of" Human', (1, 7, 10, 13)),
  441. diff --git a/src/nobilis/lifepath.py b/src/nobilis/lifepath.py
  442. new file mode 100644
  443. --- /dev/null
  444. +++ b/src/nobilis/lifepath.py
  445. @@ -0,0 +1,62 @@
  446. +import collections
  447. +
  448. +from . import visitor
  449. +
  450. +
  451. +nt = collections.namedtuple
  452. +
  453. +Business = visitor.make_visitor()
  454. +
  455. +
  456. +def key(item):
  457. +    return type(item), item
  458. +
  459. +
  460. +class Parentable:
  461. +
  462. +    __slots__ = ()
  463. +
  464. +
  465. +P = Parentable
  466. +
  467. +
  468. +class Choice(P, nt('Choice', 'template')):
  469. +
  470. +    __slots__ = ()
  471. +
  472. +
  473. +class ChoiceDisplay(P, nt('ChoiceDisplay', 'choice')):
  474. +
  475. +    __slots__ = ()
  476. +
  477. +
  478. +class ChoiceTaken(P, nt('ChoiceTaken', 'choice')):
  479. +
  480. +    __slots__ = ()
  481. +
  482. +
  483. +class ChoiceListed(P, nt('ChoiceListed', 'choice')):
  484. +
  485. +    __slots__ = ()
  486. +
  487. +
  488. +class ChoiceList(P, nt('ChoiceList', 'choices initial_min initial_max')):
  489. +
  490. +    __slots__ = ()
  491. +
  492. +
  493. +class DisplayList(P, nt('DisplayList', 'factory choices_ sections_')):
  494. +
  495. +    __slots__ = ()
  496. +
  497. +
  498. +class Section(P, nt('Section', 'display_list name')):
  499. +
  500. +    __slots__ = ()
  501. diff --git a/src/nobilis/visitor.py b/src/nobilis/visitor.py
  502. new file mode 100644
  503. --- /dev/null
  504. +++ b/src/nobilis/visitor.py
  505. @@ -0,0 +1,88 @@
  506. +"""This module provides an abstract class for visitors."""
  507. +
  508. +import functools
  509. +
  510. +SENTINEL = object()
  511. +
  512. +
  513. +def visitor_func(self, node, function, old_visitor):
  514. +    def next_visitor(arg=None):
  515. +        return function(self, node, old_visitor)
  516. +    return next_visitor
  517. +
  518. +
  519. +def identity(arg=None):
  520. +    return arg
  521. +
  522. +
  523. +def make_visitor(default=None):
  524. +    """Return a base Visitor class."""
  525. +    class Visitor:
  526. +
  527. +        __classes = set()
  528. +
  529. +        def __new__(cls, *args, **kwargs):
  530. +            if cls is Visitor:
  531. +                raise TypeError
  532. +            return super().__new__(cls, *args, **kwargs)
  533. +
  534. +        def __init_subclass__(cls, **kwargs):
  535. +            super().__init_subclass__(**kwargs)
  536. +            cls.__visitors = {}
  537. +
  538. +        if default is not None:
  539. +            default_visit = default
  540. +        else:
  541. +            def default_visit(self, node):
  542. +                raise NotImplementedError(
  543. +                    f'No implementation for {node} on {self}.')
  544. +
  545. +        def __get_visitor(self, node_class):
  546. +            my_mro = type(self).mro()
  547. +            visitor_index = my_mro.index(Visitor)
  548. +            my_mro = my_mro[:visitor_index]
  549. +            visitor_mro = []
  550. +            for cls in node_class.mro():
  551. +                for visitor_class in my_mro:
  552. +                    try:
  553. +                        visitor_dict = visitor_class.__visitors
  554. +                    except AttributeError:
  555. +                        pass
  556. +                    else:
  557. +                        visitor = visitor_dict.get(cls, SENTINEL)
  558. +                        if visitor is not SENTINEL:
  559. +                            visitor_mro.append(visitor)
  560. +            return visitor_mro
  561. +
  562. +        def visit(self, node):
  563. +            visitors = self.__get_visitor(type(node))
  564. +            if visitors:
  565. +                visitor = identity
  566. +                while visitors:
  567. +                    visitor = visitor_func(self, node, visitors.pop(), visitor)
  568. +                return visitor()
  569. +            else:
  570. +                return self.default_visit(node)
  571. +
  572. +        @classmethod
  573. +        def bind(cls, node_class, function=None):
  574. +            if cls is Visitor:
  575. +                raise NotImplementedError
  576. +            if node_class not in cls.__classes:
  577. +                raise ValueError
  578. +            if function is None:
  579. +                return functools.partial(cls.bind, node_class)
  580. +            cls.__visitors[node_class] = function
  581. +            return cls
  582. +
  583. +        @classmethod
  584. +        def mark(cls, node_class):
  585. +            cls.__classes.add(node_class)
  586. +            return node_class
  587. +
  588. +        @classmethod
  589. +        def undefined(cls):
  590. +            if cls is Visitor:
  591. +                raise ValueError
  592. +            return cls.__classes.difference(cls.__visitors)
  593. +    return Visitor
  594. diff --git a/tests/nobilis/__init__.py b/tests/nobilis/__init__.py
  595. new file mode 100644
  596. diff --git a/tests/nobilis/conftest.py b/tests/nobilis/conftest.py
  597. new file mode 100644
  598. --- /dev/null
  599. +++ b/tests/nobilis/conftest.py
  600. @@ -0,0 +1,19 @@
  601. +import pytest
  602. +
  603. +
  604. +def visitor():
  605. +    import nobilis.visitor
  606. +    return nobilis.visitor
  607. +
  608. +
  609. +def game_data():
  610. +    import nobilis.game_data
  611. +    return nobilis.game_data
  612. +
  613. +
  614. +def lifepath():
  615. +    import nobilis.lifepath
  616. +    return nobilis.lifepath
  617. diff --git a/tests/nobilis/test_game_data.py b/tests/nobilis/test_game_data.py
  618. new file mode 100644
  619. --- /dev/null
  620. +++ b/tests/nobilis/test_game_data.py
  621. @@ -0,0 +1,2 @@
  622. +def test_import(game_data):
  623. +    assert game_data
  624. diff --git a/tests/nobilis/test_lifepath.py b/tests/nobilis/test_lifepath.py
  625. new file mode 100644
  626. --- /dev/null
  627. +++ b/tests/nobilis/test_lifepath.py
  628. @@ -0,0 +1,7 @@
  629. +def test_import(lifepath):
  630. +    assert lifepath
  631. +
  632. +
  633. +def test_key(lifepath):
  634. +    assert lifepath.key(lifepath.Choice(None)) != lifepath.key(
  635. +        lifepath.ChoiceDisplay(None))
  636. diff --git a/tests/nobilis/test_visitor.py b/tests/nobilis/test_visitor.py
  637. new file mode 100644
  638. --- /dev/null
  639. +++ b/tests/nobilis/test_visitor.py
  640. @@ -0,0 +1,149 @@
  641. +import pytest
  642. +
  643. +
  644. +def test_import(visitor):
  645. +    assert visitor
  646. +
  647. +
  648. +def test_visitor(visitor):
  649. +    assert visitor.make_visitor()
  650. +
  651. +
  652. +def test_mark(visitor):
  653. +    Visitor = visitor.make_visitor()
  654. +
  655. +    @Visitor.mark
  656. +    class Test:
  657. +        pass
  658. +
  659. +    class Test2:
  660. +        pass
  661. +
  662. +    Test3 = Visitor.mark(Test2)
  663. +    assert Test2 is Test3
  664. +
  665. +
  666. +def test_cannot_instantiate_base_class(visitor):
  667. +    Visitor = visitor.make_visitor()
  668. +
  669. +    with pytest.raises(TypeError):
  670. +        print(Visitor())
  671. +
  672. +    class Subclass(Visitor):
  673. +        pass
  674. +
  675. +    assert Subclass()
  676. +
  677. +
  678. +def test_bind(visitor):
  679. +    Visitor = visitor.make_visitor()
  680. +
  681. +    @Visitor.mark
  682. +    class Test:
  683. +        pass
  684. +
  685. +    class Subclass(Visitor):
  686. +        pass
  687. +
  688. +    with pytest.raises(ValueError):
  689. +        print(Visitor.undefined())
  690. +
  691. +    assert Subclass.undefined() == {Test}
  692. +
  693. +    with pytest.raises(NotImplementedError):
  694. +        Visitor.bind(Test)
  695. +
  696. +    with pytest.raises(NotImplementedError):
  697. +        Subclass().visit(Test())
  698. +
  699. +    @Subclass.bind(Test)
  700. +    def Subclass(self, node, next_visitor):
  701. +        return node
  702. +
  703. +    assert Subclass.undefined() == set()
  704. +
  705. +    test = Test()
  706. +    assert Subclass().visit(test) is test
  707. +
  708. +
  709. +def test_provide_default(visitor):
  710. +    @visitor.make_visitor
  711. +    def Visitor(self, node):
  712. +        return node
  713. +
  714. +    @Visitor.mark
  715. +    class Test:
  716. +        pass
  717. +
  718. +    class Subclass(Visitor):
  719. +        pass
  720. +
  721. +    test = Test()
  722. +    assert Subclass().visit(test) is test
  723. +
  724. +
  725. +def test_cannot_add_unmarked(visitor):
  726. +    Visitor = visitor.make_visitor()
  727. +
  728. +    class Subclass(Visitor):
  729. +        pass
  730. +
  731. +    with pytest.raises(ValueError):
  732. +        Subclass.bind(object)
  733. +
  734. +    Visitor.mark(object)
  735. +
  736. +    assert Subclass.bind(object)
  737. +
  738. +
  739. +def test_multiple_inheritance(visitor):
  740. +    Visitor = visitor.make_visitor()
  741. +
  742. +    class Klass:
  743. +        pass
  744. +
  745. +    class Subclass(Klass, Visitor):
  746. +        pass
  747. +
  748. +    @Visitor.mark
  749. +    class Test:
  750. +        pass
  751. +
  752. +    with pytest.raises(NotImplementedError):
  753. +        Subclass().visit(Test())
  754. +
  755. +
  756. +def test_next_visitor(visitor):
  757. +    Visitor = visitor.make_visitor()
  758. +
  759. +    @Visitor.mark
  760. +    class Test1:
  761. +        pass
  762. +
  763. +    @Visitor.mark
  764. +    class Test2(Test1):
  765. +        pass
  766. +
  767. +    class Visitor1(Visitor):
  768. +        pass
  769. +
  770. +    class Visitor2(Visitor1):
  771. +        pass
  772. +
  773. +    @Visitor2.bind(Test2)
  774. +    def Visitor2(self, node, next_visitor):
  775. +        return 'a' + next_visitor('f')
  776. +
  777. +    @Visitor1.bind(Test2)
  778. +    def Visitor1(self, node, next_visitor):
  779. +        return 'b' + next_visitor('f')
  780. +
  781. +    @Visitor2.bind(Test1)
  782. +    def Visitor2(self, node, next_visitor):
  783. +        return 'c' + next_visitor('f')
  784. +
  785. +    @Visitor1.bind(Test1)
  786. +    def Visitor1(self, node, next_visitor):
  787. +        return 'd' + next_visitor('e')
  788. +
  789. +    assert Visitor2().visit(Test2()) == 'abcde'
  790. diff --git a/tests/test_nobilis.py b/tests/test_nobilis.py
  791. new file mode 100644
  792. --- /dev/null
  793. +++ b/tests/test_nobilis.py
  794. @@ -0,0 +1,3 @@
  795. +def test_import_nobilis():
  796. +    import nobilis
  797. +    assert nobilis
  798. diff --git a/tox.ini b/tox.ini
  799. new file mode 100644
  800. --- /dev/null
  801. +++ b/tox.ini
  802. @@ -0,0 +1,17 @@
  803. +# Tox (http://tox.testrun.org/) is a tool for running tests
  804. +# in multiple virtualenvs. This configuration file will run the
  805. +# test suite on all supported python versions. To use it, "pip install tox"
  806. +# and then run "tox" from this directory.
  807. +
  808. +[tox]
  809. +envlist = py36
  810. +
  811. +[testenv]
  812. +commands =
  813. +    {envpython} -m divide_and_cover run -m pytest --divide-and-cover
  814. +    {envpython} -m divide_and_cover html -d htmlcov/{envname}
  815. +deps =
  816. +    pytest-divide-and-cover
  817. +    divide-and-cover
  818. +    pytest
  819. +    coverage
Advertisement
Add Comment
Please, Sign In to add comment