mwchase

LC:Nobilis Update 5: diff

May 28th, 2017
1,341
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Diff 12.64 KB | None | 0 0
  1. diff --git a/setup.py b/setup.py
  2. --- a/setup.py
  3. +++ b/setup.py
  4. @@ -40,8 +40,4 @@
  5.  
  6.      packages=find_packages('src', exclude=['contrib', 'docs', 'tests']),
  7.      package_dir={'': 'src'},
  8. -
  9. -    extras_require={
  10. -        'test': ['coverage', 'pytest'],
  11. -    },
  12.  )
  13. diff --git a/src/nobilis/lifepath.py b/src/nobilis/lifepath.py
  14. --- a/src/nobilis/lifepath.py
  15. +++ b/src/nobilis/lifepath.py
  16. @@ -1,5 +1,8 @@
  17.  import collections
  18. +import functools
  19.  
  20. +from . import map_tuple
  21. +from . import mapping
  22.  from . import visitor
  23.  
  24.  
  25. @@ -12,51 +15,133 @@
  26.      return type(item), item
  27.  
  28.  
  29. -class Parentable:
  30. +# This is horrendously brittle, but I don't feel like writing test cases right
  31. +# now. Basically, it needs to be used as a mixin AHEAD OF a namedtuple class
  32. +# with a first field of "hash".
  33. +class QuickHash:
  34. +
  35. +    __slots__ = ()
  36. +
  37. +    def __new__(cls, *args, **kwargs):
  38. +        my_hash = cls._pre_hash(*args, **kwargs)
  39. +        return super().__new__(cls, my_hash, *args, **kwargs)
  40. +
  41. +    @classmethod
  42. +    def _pre_hash(cls, *args, **kwargs):
  43. +        args = tuple(args)
  44. +        fields = getattr(cls, '_fields', ('hash',))[1:]
  45. +        keywords = fields[len(args):]
  46. +        args += tuple((kwargs[keyword] for keyword in keywords))
  47. +        return hash(args)
  48. +
  49. +    def __hash__(self):
  50. +        return self.hash
  51. +
  52. +
  53. +QH = QuickHash
  54. +
  55. +
  56. +class Child(Business):
  57.  
  58.      __slots__ = ()
  59.  
  60. +    def default_visit(self, node):
  61. +        yield from ()
  62.  
  63. -P = Parentable
  64. +
  65. +CHILD = Child()
  66. +
  67. +
  68. +class Tree(QH, nt('Tree', 'hash root reverse_mapping')):
  69. +
  70. +    __slots__ = ()
  71. +
  72. +    def __new__(cls, root):
  73. +        reverse_mapping = {}
  74. +        nodes_to_add = [root]
  75. +        while nodes_to_add:
  76. +            node = nodes_to_add.pop()
  77. +            for child in CHILD.visit(node):
  78. +                child_key = key(child)
  79. +                if child_key in reverse_mapping:
  80. +                    raise ValueError(f'{root} is not a valid tree!')
  81. +                else:
  82. +                    reverse_mapping[child_key] = node
  83. +                nodes_to_add.append(child)
  84. +        return super().__new__(cls, root, mapping.Map(reverse_mapping))
  85. +
  86. +    def parent(self, node):
  87. +        return self.reverse_mapping.get(key(node))
  88.  
  89.  
  90.  @Business.mark
  91. -class Choice(P, nt('Choice', 'template')):
  92. -
  93. -    __slots__ = ()
  94. -
  95. -
  96. -class ChoiceDisplay(P, nt('ChoiceDisplay', 'choice')):
  97. +class Choice(QH, nt('Choice', 'hash template')):
  98.  
  99.      __slots__ = ()
  100.  
  101.  
  102.  @Business.mark
  103. -class ChoiceTaken(P, nt('ChoiceTaken', 'choice')):
  104. +class ChoiceDisplay(QH, nt('ChoiceDisplay', 'hash choice')):
  105.  
  106.      __slots__ = ()
  107.  
  108.  
  109.  @Business.mark
  110. -class ChoiceListed(P, nt('ChoiceListed', 'choice')):
  111. +class ChoiceTaken(QH, nt('ChoiceTaken', 'hash choice')):
  112. +
  113. +    __slots__ = ()
  114. +
  115. +
  116. +class ChoiceListed(QH, nt('ChoiceListed', 'hash choice')):
  117. +
  118. +    __slots__ = ()
  119. +
  120. +
  121. +class ChoiceList(QH, nt('ChoiceList', 'hash choices initial_min initial_max')):
  122.  
  123.      __slots__ = ()
  124.  
  125.  
  126.  @Business.mark
  127. -class ChoiceList(P, nt('ChoiceList', 'choices initial_min initial_max')):
  128. +class DisplayList(QH, nt('DisplayList', 'hash factory choices_ sections_')):
  129.  
  130.      __slots__ = ()
  131.  
  132. +    @property
  133. +    def choices(self):
  134. +        return map_tuple.MapTuple(self.factory, self.choices_)
  135. +
  136. +    @property
  137. +    def sections(self):
  138. +        return map_tuple.MapTuple(
  139. +            functools.partial(Section, self), range(len(self.sections_)))
  140. +
  141.  
  142.  @Business.mark
  143. -class DisplayList(P, nt('DisplayList', 'factory choices_ sections_')):
  144. +class Section(QH, nt('Section', 'hash display_list index')):
  145.  
  146.      __slots__ = ()
  147.  
  148. +    @property
  149. +    def _data(self):
  150. +        return self.display_list.sections_[self.index]
  151.  
  152. -class Section(P, nt('Section', 'display_list name')):
  153. +    @property
  154. +    def name(self):
  155. +        return self._data[0]
  156. +
  157. +    @property
  158. +    def predicate(self):
  159. +        return self._data[1]
  160. +
  161.  
  162. -    __slots__ = ()
  163. [email protected](ChoiceList)
  164. +def Child(self, node, next_visitor):
  165. +    yield from node.choices
  166. +
  167. +
  168. [email protected](DisplayList)
  169. +def Child(self, node, next_visitor):
  170. +    yield from node.choices
  171. diff --git a/src/nobilis/map_tuple.py b/src/nobilis/map_tuple.py
  172. new file mode 100644
  173. --- /dev/null
  174. +++ b/src/nobilis/map_tuple.py
  175. @@ -0,0 +1,16 @@
  176. +import collections
  177. +
  178. +
  179. +class MapTuple(collections.Sequence):
  180. +
  181. +    __slots__ = '_function', '_sequence'
  182. +
  183. +    def __init__(self, function, sequence):
  184. +        self._function = function
  185. +        self._sequence = sequence
  186. +
  187. +    def __len__(self):
  188. +        return len(self._sequence)
  189. +
  190. +    def __getitem__(self, index):
  191. +        return self._function(self._sequence[index])
  192. diff --git a/src/nobilis/mapping.py b/src/nobilis/mapping.py
  193. new file mode 100644
  194. --- /dev/null
  195. +++ b/src/nobilis/mapping.py
  196. @@ -0,0 +1,28 @@
  197. +import collections
  198. +
  199. +
  200. +class Map(collections.abc.Mapping):
  201. +
  202. +    __slots__ = '_dict', '_hash'
  203. +
  204. +    def __init__(*args, **kwargs):
  205. +        try:
  206. +            self, *args = args
  207. +        except ValueError:
  208. +            raise TypeError
  209. +        self._hash = None
  210. +        self._dict = dict(*args, **kwargs)
  211. +
  212. +    def __hash__(self):
  213. +        if self._hash is None:
  214. +            self._hash = hash(frozenset(self._dict.items()))
  215. +        return self._hash
  216. +
  217. +    def __getitem__(self, key):
  218. +        return self._dict[key]
  219. +
  220. +    def __iter__(self):
  221. +        return iter(self._dict)
  222. +
  223. +    def __len__(self):
  224. +        return len(self._dict)
  225. diff --git a/src/nobilis/visitor.py b/src/nobilis/visitor.py
  226. --- a/src/nobilis/visitor.py
  227. +++ b/src/nobilis/visitor.py
  228. @@ -19,6 +19,8 @@
  229.      """Return a base Visitor class."""
  230.      class Visitor:
  231.  
  232. +        __slots__ = ()
  233. +
  234.          __classes = set()
  235.  
  236.          def __new__(cls, *args, **kwargs):
  237. diff --git a/tests/nobilis/conftest.py b/tests/nobilis/conftest.py
  238. --- a/tests/nobilis/conftest.py
  239. +++ b/tests/nobilis/conftest.py
  240. @@ -17,3 +17,15 @@
  241.  def lifepath():
  242.      import nobilis.lifepath
  243.      return nobilis.lifepath
  244. +
  245. +
  246. +def mapping():
  247. +    import nobilis.mapping
  248. +    return nobilis.mapping
  249. +
  250. +
  251. +def map_tuple():
  252. +    import nobilis.map_tuple
  253. +    return nobilis.map_tuple
  254. diff --git a/tests/nobilis/test_lifepath.py b/tests/nobilis/test_lifepath.py
  255. --- a/tests/nobilis/test_lifepath.py
  256. +++ b/tests/nobilis/test_lifepath.py
  257. @@ -1,3 +1,6 @@
  258. +import pytest
  259. +
  260. +
  261.  def test_import(lifepath):
  262.      assert lifepath
  263.  
  264. @@ -5,3 +8,108 @@
  265.  def test_key(lifepath):
  266.      assert lifepath.key(lifepath.Choice(None)) != lifepath.key(
  267.          lifepath.ChoiceDisplay(None))
  268. +
  269. +
  270. +def test_hash_amortization(lifepath):
  271. +    # This is a little tricky. Basically, I don't want hashing a recursive type
  272. +    # to hit the leaves repeatedly. Of course, a leaf has to be hashed at least
  273. +    # once. But let's give this a shot.
  274. +    class TrackingChoice(lifepath.Choice):
  275. +
  276. +        hash_count = 0
  277. +
  278. +        def __hash__(self):
  279. +            self.hash_count += 1
  280. +            return super().__hash__()
  281. +
  282. +    leaf = TrackingChoice(None)
  283. +    assert leaf.hash_count == 0
  284. +    root = lifepath.Choice(leaf)
  285. +    assert leaf.hash_count <= 1
  286. +    for _ in range(3):
  287. +        hash(root)
  288. +        assert leaf.hash_count <= 1
  289. +
  290. +
  291. +# So, my current plan for setting up the "parent" relationship is to lean on
  292. +# the visitors, in order to avoid putting more logic onto the classes
  293. +# themselves. Basically, each class defines its own concept of "children", and
  294. +# there's a visitor or a protocol that defines and enforces the reverse
  295. +# relationship. I think a non-visitor would be fine, since it simply has to
  296. +# consume the child visitor, which defaults to yield from (), and defines
  297. +# actual behavior for specific classes.
  298. +
  299. +# To speed up construction of the parent graph, each component could cache a
  300. +# mapping of its data, which could be pulled in directly. What's a good dict
  301. +# idiom for "update, but fail on collision"?
  302. +
  303. +# Here's something I think would make sense: rather than a global cache, the
  304. +# "parent" instances have an OrderedDict attribute that they share when
  305. +# creating other instances from themselves. The OrderedDict is accessed through
  306. +# synchronized functions, that pop and push the requested value. When it's
  307. +# larger than some threshold, the first few entries are removed. This is like
  308. +# an LRU, but with shared state rather than global state, so it's more
  309. +# testable. Have a wrapper class in its own module, just for managing this
  310. +# stuff.
  311. +
  312. +# Actually, given all that, I could just make it an instance attribute that's
  313. +# an LRU. That seems pretty simple.
  314. +
  315. +# But for now, I'll just construct the dictionary per-instance, no funny stuff.
  316. +
  317. +
  318. +def test_children(lifepath):
  319. +    child = lifepath.Child()
  320. +
  321. +    leaf = lifepath.Choice(None)
  322. +
  323. +    assert tuple(child.visit(leaf)) == ()
  324. +
  325. +    for wrapper in (lifepath.ChoiceDisplay, lifepath.ChoiceTaken,
  326. +                    lifepath.ChoiceListed):
  327. +        assert tuple(child.visit(wrapper(leaf))) == ()
  328. +
  329. +    assert tuple(child.visit(lifepath.ChoiceList((leaf,), 0, None))) == (leaf,)
  330. +
  331. +    display_list = lifepath.DisplayList(
  332. +        lifepath.ChoiceDisplay, (leaf,), (('name', None),))
  333. +
  334. +    assert tuple(child.visit(display_list)) == (lifepath.ChoiceDisplay(leaf),)
  335. +
  336. +
  337. +def test_tree(lifepath):
  338. +    leaf = lifepath.Choice(None)
  339. +    root = lifepath.ChoiceList((leaf,), 0, None)
  340. +    tree = lifepath.Tree(root)
  341. +    assert tree.parent(leaf) == root
  342. +
  343. +    rooter = lifepath.ChoiceList((root,), 0, None)
  344. +
  345. +    treeer = lifepath.Tree(rooter)
  346. +    assert treeer.parent(root) == rooter
  347. +    assert treeer.parent(leaf) == root
  348. +
  349. +    dag = lifepath.ChoiceList((leaf, leaf), 0, None)
  350. +
  351. +    with pytest.raises(ValueError):
  352. +        print(lifepath.Tree(dag))
  353. +
  354. +
  355. +def test_section(lifepath):
  356. +    leaf = lifepath.Choice(None)
  357. +    sentinel = object()
  358. +
  359. +    display_list = lifepath.DisplayList(
  360. +        lifepath.ChoiceDisplay, (leaf,), (('name', sentinel),))
  361. +
  362. +    section, = display_list.sections
  363. +
  364. +    assert section.name == 'name'
  365. +    assert section.predicate is sentinel
  366. +
  367. +
  368. +def test_state(lifepath):
  369. +    # Okay, so, let's think how this has to go.
  370. +    # Ideally, I wouldn't be relying on mutable state, so...
  371. +    # state = lifepath.State()
  372. +    pass
  373. diff --git a/tests/nobilis/test_map_tuple.py b/tests/nobilis/test_map_tuple.py
  374. new file mode 100644
  375. --- /dev/null
  376. +++ b/tests/nobilis/test_map_tuple.py
  377. @@ -0,0 +1,12 @@
  378. +def test_import(map_tuple):
  379. +    assert map_tuple
  380. +
  381. +
  382. +def test_class(map_tuple):
  383. +    assert map_tuple.MapTuple
  384. +
  385. +
  386. +def test_basic(map_tuple):
  387. +    my_tuple = map_tuple.MapTuple(str.upper, ('abc',))
  388. +    assert len(my_tuple) == 1
  389. +    assert my_tuple[0] == 'ABC'
  390. diff --git a/tests/nobilis/test_mapping.py b/tests/nobilis/test_mapping.py
  391. new file mode 100644
  392. --- /dev/null
  393. +++ b/tests/nobilis/test_mapping.py
  394. @@ -0,0 +1,25 @@
  395. +import pytest
  396. +
  397. +
  398. +def test_import(mapping):
  399. +    assert mapping
  400. +
  401. +
  402. +def test_map(mapping):
  403. +    assert not mapping.Map()
  404. +    assert not mapping.Map({})
  405. +
  406. +    assert mapping.Map() == mapping.Map({})
  407. +
  408. +    assert mapping.Map(self=1) == mapping.Map(self=1)
  409. +    assert {mapping.Map()}
  410. +    a_mapping = mapping.Map()
  411. +    dct = {a_mapping: a_mapping}
  412. +    assert dct[a_mapping] == a_mapping
  413. +
  414. +
  415. +def test_a_weird_edge(mapping):
  416. +    with pytest.raises(TypeError):
  417. +        print(dict.__init__())
  418. +    with pytest.raises(TypeError):
  419. +        print(mapping.Map.__init__())
  420. diff --git a/tests/nobilis/test_visitor.py b/tests/nobilis/test_visitor.py
  421. --- a/tests/nobilis/test_visitor.py
  422. +++ b/tests/nobilis/test_visitor.py
  423. @@ -147,3 +147,43 @@
  424.          return 'd' + next_visitor('e')
  425.  
  426.      assert Visitor2().visit(Test2()) == 'abcde'
  427. +
  428. +
  429. +def test_next_visitor_as_generator(visitor):
  430. +    Visitor = visitor.make_visitor()
  431. +
  432. +    @Visitor.mark
  433. +    class Test1:
  434. +        pass
  435. +
  436. +    @Visitor.mark
  437. +    class Test2(Test1):
  438. +        pass
  439. +
  440. +    class Visitor1(Visitor):
  441. +        pass
  442. +
  443. +    class Visitor2(Visitor1):
  444. +        pass
  445. +
  446. +    @Visitor2.bind(Test2)
  447. +    def Visitor2(self, node, next_visitor):
  448. +        yield 'a'
  449. +        yield from next_visitor('f')
  450. +
  451. +    @Visitor1.bind(Test2)
  452. +    def Visitor1(self, node, next_visitor):
  453. +        yield 'b'
  454. +        yield from next_visitor('f')
  455. +
  456. +    @Visitor2.bind(Test1)
  457. +    def Visitor2(self, node, next_visitor):
  458. +        yield 'c'
  459. +        yield from next_visitor('f')
  460. +
  461. +    @Visitor1.bind(Test1)
  462. +    def Visitor1(self, node, next_visitor):
  463. +        yield 'd'
  464. +        yield from next_visitor('e')
  465. +
  466. +    assert ''.join(Visitor2().visit(Test2())) == 'abcde'
Advertisement
Add Comment
Please, Sign In to add comment