Skip to content

Commit

Permalink
Merge pull request #243 from tcmitchell/241-visitor
Browse files Browse the repository at this point in the history
Implement visitor pattern
  • Loading branch information
tcmitchell authored Apr 30, 2021
2 parents 32a474e + 7c0235c commit 80fd20c
Show file tree
Hide file tree
Showing 32 changed files with 672 additions and 125 deletions.
1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ and constructing genetic designs according to the standardized specifications of
introduction
installation
extensions
visitor
ontology
.. getting_started
.. repositories
Expand Down
4 changes: 2 additions & 2 deletions docs/repositories.rst
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,9 @@ The ``pull`` operation will retrieve ``ComponentDefinitions`` and their associat
.. end
--------------------
------------------------
Logging in to Part Repos
--------------------
------------------------

Some parts repositories can be accessed as above, without
authenticating to the parts repository. You may also have access to
Expand Down
108 changes: 108 additions & 0 deletions docs/visitor.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
Using Visitors
==============

The Visitor Pattern is a well known and commonly used pattern for
performing an operation on the elements of an object structure. There
are many online resources for learning about the visitor pattern.

The implementation of the Visitor Pattern in pySBOL3 is very
simple. When a pySBOL3 object's `accept` method is called, a visitor
is passed as the only arugment. The `accept` method, in turn, invokes
`visit_type` on the visitor, passing the pySBOL3 object as the only
argument.

Traversal of the pySBOL3 object graph is left to the visitor
itself. When a `visit_type` method is called, the visitor must then
invoke `accept` on any child objects it might want to visit. See the
code sample below for an example of this traversal.


Resources
---------
* https://sourcemaking.com/design_patterns/visitor
* https://refactoring.guru/design-patterns/visitor
* https://www.informit.com/store/design-patterns-elements-of-reusable-object-oriented-9780201633610


Example Code
------------

The program below will visit each top level object in the document as
well as visiting any features on the top level components. Note that
the visitor must direct the traversal of the features, as discussed
above.

.. code:: python
import sbol3
class MyVisitor:
def visit_component(self, c: sbol3.Component):
print(f'Component {c.identity}')
for f in c.features:
f.accept(self)
def visit_sequence(self, s: sbol3.Component):
print(f'Sequence {s.identity}')
def visit_sub_component(self, sc: sbol3.Component):
print(f'SubComponent {sc.identity}')
doc = sbol3.Document()
doc.read('test/SBOLTestSuite/SBOL3/BBa_F2620_PoPSReceiver/BBa_F2620_PoPSReceiver.ttl')
visitor = MyVisitor()
for obj in doc.objects:
obj.accept(visitor)
.. end
Visit Methods
-------------

The table below lists each class that has an accept method and the
corresponding method that is invoked on the visitor passed to the
accept method.

======================= ============
Class Visit Method
======================= ============
Activity visit_activity
Agent visit_agent
Association visit_association
Attachment visit_attachment
BinaryPrefix visit_binary_prefix
Collection visit_collection
CombinatorialDerivation visit_combinatorial_derivation
Component visit_component
ComponentReference visit_component_reference
Constraint visit_constraint
Cut visit_cut
Document visit_document(self)
EntireSequence visit_entire_sequence
Experiment visit_experiment
ExperimentalData visit_experimental_data
ExternallyDefined visit_externally_defined
Implementation visit_implementation
Interaction visit_interaction
Interface visit_interface
LocalSubComponent visit_local_sub_component
Measure visit_measure
Model visit_model
Participation visit_participation
Plan visit_plan
PrefixedUnit visit_prefixed_unit
Range visit_range
SIPrefix visit_si_prefix
Sequence visit_sequence
SequenceFeature visit_sequence_feature
SingularUnit visit_singular_unit
SubComponent visit_sub_component
UnitDivision visit_unit_division
UnitExponentiation visit_unit_exponentiation
UnitMultiplication visit_unit_multiplication
Usage visit_usage
VariableFeature visit_variable_feature
======================= ============
64 changes: 15 additions & 49 deletions examples/visitor.py
Original file line number Diff line number Diff line change
@@ -1,69 +1,35 @@
import argparse
import logging
from typing import Any

import sbol3

# This example demonstrates how to adapt the functional vistor pattern
# in pySBOL3 to an object oriented visitor pattern similar to
# https://refactoring.guru/design-patterns/visitor/python/example or
# https://en.wikipedia.org/wiki/Visitor_pattern#Python_example

# This example demonstrates how to use the visitor pattern
# in pySBOL3 to navigate a document
#
# Usage:
#
# python3 visitor.py [-d] SBOL_FILE_NAME


class SBOLVisitor:
"""A base class for visitor pySBOL3 visitors.
"""

def __call__(self, sbol_object: sbol3.Identified):
"""This method is invoked on every SBOL object that is visited. The
visit is then dispatched to a type-specific method. If no
type-specific method exists, `visit_fallback` is invoked with
the SBOL object. The default implementation of
`visit_fallback` does nothing.
"""
method_name = self._method_name(sbol_object)
try:
getattr(self, method_name)(sbol_object)
except AttributeError:
self.visit_fallback(sbol_object)

def _method_name(self, thing: Any) -> str:
"""Generates a visitor method name for a given object. Override this
method if you want a different conversion from object to
method name.
"""
qualname = type(thing).__qualname__.replace(".", "_")
return f'visit_{qualname}'

def visit_fallback(self, sbol_object: sbol3.Identified):
"""Visit an object that doesn't have a specific method for its type.
Override this method to catch objects that do not have a
specific visit method defined for them.
"""
pass


class MyVisitor(SBOLVisitor):
class MyVisitor:
"""An example visitor.
"""

def visit_Component(self, c):
def visit_document(self, doc: sbol3.Document):
for obj in doc.objects:
obj.accept(self)

def visit_component(self, c: sbol3.Component):
for feature in c.features:
feature.accept(self)
print(f'Visited Component {c.identity}')

def visit_SubComponent(self, sc):
print(f'Visited SubComponent {sc.identity}')
def visit_component_reference(self, cr: sbol3.ComponentReference):
print(f'Visited ComponentReference {cr.identity}')

def visit_fallback(self, sbol_object: sbol3.Identified):
type_name = type(sbol_object).__qualname__
logging.debug(f'Fallback visit of {type_name} {sbol_object.identity}')
def visit_sub_component(self, sc: sbol3.SubComponent):
print(f'Visited SubComponent {sc.identity}')


def parse_args(args=None):
Expand Down
15 changes: 14 additions & 1 deletion sbol3/attachment.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import List
from typing import List, Any

from . import *

Expand Down Expand Up @@ -44,6 +44,19 @@ def validate(self, report: ValidationReport = None) -> ValidationReport:
report.addError(self.identity, None, message)
return report

def accept(self, visitor: Any) -> Any:
"""Invokes `visit_attachment` on `visitor` with `self` as the only
argument.
:param visitor: The visitor instance
:type visitor: Any
:raises AttributeError: If visitor lacks a visit_attachment method
:return: Whatever `visitor.visit_attachment` returns
:rtype: Any
"""
visitor.visit_attachment(self)


def build_attachment(identity: str,
*, type_uri: str = SBOL_COMPONENT) -> SBOLObject:
Expand Down
28 changes: 27 additions & 1 deletion sbol3/collection.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import math
from typing import List
from typing import List, Any

from . import *

Expand Down Expand Up @@ -37,6 +37,19 @@ def __init__(self, identity: str,
self.members = ReferencedObject(self, SBOL_MEMBER, 0, math.inf,
initial_value=members)

def accept(self, visitor: Any) -> Any:
"""Invokes `visit_collection` on `visitor` with `self` as the only
argument.
:param visitor: The visitor instance
:type visitor: Any
:raises AttributeError: If visitor lacks a visit_collection method
:return: Whatever `visitor.visit_collection` returns
:rtype: Any
"""
visitor.visit_collection(self)


class Experiment(Collection):
"""The purpose of the Experiment class is to aggregate
Expand All @@ -62,6 +75,19 @@ def __init__(self, identity: str,
description=description, derived_from=derived_from,
generated_by=generated_by, measures=measures)

def accept(self, visitor: Any) -> Any:
"""Invokes `visit_experiment` on `visitor` with `self` as the only
argument.
:param visitor: The visitor instance
:type visitor: Any
:raises AttributeError: If visitor lacks a visit_experiment method
:return: Whatever `visitor.visit_experiment` returns
:rtype: Any
"""
visitor.visit_experiment(self)


Document.register_builder(SBOL_COLLECTION, Collection)
Document.register_builder(SBOL_EXPERIMENT, Experiment)
16 changes: 15 additions & 1 deletion sbol3/combderiv.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import math
from typing import Union, List
from typing import Union, List, Any

from . import *

Expand Down Expand Up @@ -50,6 +50,20 @@ def validate(self, report: ValidationReport = None) -> ValidationReport:
report.addError(self.identity, None, message)
return report

def accept(self, visitor: Any) -> Any:
"""Invokes `visit_combinatorial_derivation` on `visitor` with `self`
as the only argument.
:param visitor: The visitor instance
:type visitor: Any
:raises AttributeError: If visitor lacks a visit_combinatorial_derivation
method
:return: Whatever `visitor.visit_combinatorial_derivation` returns
:rtype: Any
"""
visitor.visit_combinatorial_derivation(self)


def build_combinatorial_derivation(identity: str,
*, type_uri: str = SBOL_COMBINATORIAL_DERIVATION):
Expand Down
15 changes: 14 additions & 1 deletion sbol3/component.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import math
from typing import List, Union
from typing import List, Union, Any

from . import *

Expand Down Expand Up @@ -47,6 +47,19 @@ def __init__(self, identity: str, types: Union[List[str], str],
self.models = ReferencedObject(self, SBOL_MODELS, 0, math.inf,
initial_value=models)

def accept(self, visitor: Any) -> Any:
"""Invokes `visit_component` on `visitor` with `self` as the only
argument.
:param visitor: The visitor instance
:type visitor: Any
:raises AttributeError: If visitor lacks a visit_component method
:return: Whatever `visitor.visit_component` returns
:rtype: Any
"""
visitor.visit_component(self)


def build_component(identity: str, *, type_uri: str = SBOL_COMPONENT) -> SBOLObject:
missing = PYSBOL3_MISSING
Expand Down
16 changes: 15 additions & 1 deletion sbol3/compref.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Union, List
from typing import Union, List, Any

from . import *
# Feature is not exported
Expand Down Expand Up @@ -41,6 +41,20 @@ def validate(self, report: ValidationReport = None) -> ValidationReport:
report.addError(self.identity, None, message)
return report

def accept(self, visitor: Any) -> Any:
"""Invokes `visit_component_reference` on `visitor` with `self` as the
only argument.
:param visitor: The visitor instance
:type visitor: Any
:raises AttributeError: If visitor lacks a visit_component_reference
method
:return: Whatever `visitor.visit_component_reference` returns
:rtype: Any
"""
visitor.visit_component_reference(self)


def build_component_reference(identity: str, *,
type_uri: str = SBOL_COMPONENT_REFERENCE) -> SBOLObject:
Expand Down
15 changes: 14 additions & 1 deletion sbol3/constraint.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Optional, Union, List
from typing import Optional, Union, List, Any

from . import *

Expand Down Expand Up @@ -47,6 +47,19 @@ def validate(self, report: ValidationReport = None) -> ValidationReport:
report.addError(self.identity, None, message)
return report

def accept(self, visitor: Any) -> Any:
"""Invokes `visit_constraint` on `visitor` with `self` as the only
argument.
:param visitor: The visitor instance
:type visitor: Any
:raises AttributeError: If visitor lacks a visit_constraint method
:return: Whatever `visitor.visit_constraint` returns
:rtype: Any
"""
visitor.visit_constraint(self)


def build_constraint(identity: str, type_uri: str = SBOL_CONSTRAINT) -> SBOLObject:
missing = PYSBOL3_MISSING
Expand Down
Loading

0 comments on commit 80fd20c

Please sign in to comment.