├── LICENSE.txt ├── charlatan ├── tests │ ├── __init__.py │ ├── data │ │ ├── empty.yaml │ │ ├── test.json │ │ ├── unicode.yaml │ │ ├── strings.yaml │ │ ├── simple.yaml │ │ ├── cyclic_dependencies.yaml │ │ ├── lists.yaml │ │ ├── dependencies.yaml │ │ ├── special_tags.yaml │ │ ├── relationships_without_models.yaml │ │ └── relationships.yaml │ ├── example │ │ ├── __init__.py │ │ ├── data │ │ │ ├── custom_builder.yaml │ │ │ ├── sqlalchemy.yaml │ │ │ └── deep_inherit.yaml │ │ ├── test_deep_inherit.py │ │ ├── test_custom_builder.py │ │ └── test_sqlalchemy.py │ ├── fixtures │ │ ├── __init__.py │ │ ├── simple_models.py │ │ └── models.py │ ├── test_simple_testcase.py │ ├── test_fixture_collection.py │ ├── test_utils.py │ ├── test_richgetter.py │ ├── test_lists_of_fixtures.py │ ├── test_relationships_without_models.py │ ├── test_depgraph.py │ ├── test_sqlalchemy.py │ ├── test_testcase.py │ ├── test_file_format.py │ └── test_fixtures_manager.py ├── __init__.py ├── testing.py ├── _compat.py ├── testcase.py ├── depgraph.py ├── builder.py ├── file_format.py ├── fixture_collection.py ├── utils.py ├── fixture.py └── fixtures_manager.py ├── requirements.txt ├── .coveragerc ├── docs ├── changelog.rst ├── examples │ ├── fixtures_id.yaml │ ├── files.yaml │ ├── fixtures_dict.yaml │ ├── dependencies.yaml │ ├── simple_fixtures.yaml │ ├── fixtures.yaml │ ├── fixtures_inheritance.yaml │ ├── relationships.yaml │ └── collection.yaml ├── contributing.rst ├── installation.rst ├── database.rst ├── api-reference.rst ├── builders.rst ├── hooks.rst ├── index.rst ├── make.bat ├── Makefile ├── quickstart.rst ├── conf.py └── file-format.rst ├── MANIFEST.in ├── tox.ini ├── setup.cfg ├── requirements-dev.txt ├── .travis.yml ├── .gitignore ├── requirements-test.txt ├── Makefile ├── README.rst ├── setup.py └── CHANGES.rst /LICENSE.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /charlatan/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /charlatan/tests/data/empty.yaml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /charlatan/tests/data/test.json: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /charlatan/tests/example/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /charlatan/tests/fixtures/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | PyYAML>=3.10 2 | pytz 3 | -------------------------------------------------------------------------------- /charlatan/tests/data/unicode.yaml: -------------------------------------------------------------------------------- 1 | €a: €b 2 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | omit = charlatan/_compat.py 3 | -------------------------------------------------------------------------------- /docs/changelog.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../CHANGES.rst 2 | -------------------------------------------------------------------------------- /charlatan/tests/data/strings.yaml: -------------------------------------------------------------------------------- 1 | foo: bar 2 | baz: foobar 3 | -------------------------------------------------------------------------------- /charlatan/tests/data/simple.yaml: -------------------------------------------------------------------------------- 1 | fixture: 2 | fields: 3 | now: !now -1d 4 | -------------------------------------------------------------------------------- /docs/examples/fixtures_id.yaml: -------------------------------------------------------------------------------- 1 | toaster_already_in_db: 2 | id: 10 3 | model: Toaster 4 | -------------------------------------------------------------------------------- /docs/examples/files.yaml: -------------------------------------------------------------------------------- 1 | toaster: 2 | model: Toaster 3 | fields: 4 | color: !rel relationships.toaster.color 5 | -------------------------------------------------------------------------------- /docs/examples/fixtures_dict.yaml: -------------------------------------------------------------------------------- 1 | fixture_name: 2 | fields: 3 | foo: bar 4 | 5 | fixture_list: 6 | fields: 7 | - "foo" 8 | - "bar" 9 | -------------------------------------------------------------------------------- /charlatan/tests/example/data/custom_builder.yaml: -------------------------------------------------------------------------------- 1 | toaster: 2 | model: charlatan.tests.example.test_custom_builder:Toaster 3 | fields: 4 | slots: 3 5 | color: blue 6 | -------------------------------------------------------------------------------- /docs/contributing.rst: -------------------------------------------------------------------------------- 1 | Contributing 2 | ============ 3 | 4 | Install the requirements:: 5 | 6 | $ make bootstrap 7 | 8 | Run the tests:: 9 | 10 | $ make test 11 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include charlatan * 2 | recursive-include docs * 3 | include * 4 | global-exclude *.pyc 5 | 6 | include LICENSE.txt 7 | include README.rst 8 | include requirements.txt 9 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py27, py33 3 | 4 | [testenv] 5 | deps=-rrequirements.txt 6 | -rrequirements-test.txt 7 | commands=flake8 charlatan 8 | py.test {posargs} 9 | usedevelop=True 10 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | exclude = build/ 3 | ignore = D100,D101,D102,D103 4 | 5 | [pytest] 6 | addopts = --doctest-modules --doctest-glob='*.rst' --ignore docs/examples/* --tb native --ignore=setup.py 7 | norecursedirs = .tox 8 | -------------------------------------------------------------------------------- /charlatan/__init__.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa 2 | 3 | from charlatan.fixtures_manager import FixturesManager 4 | from charlatan.testcase import FixturesManagerMixin 5 | from charlatan.fixture import Fixture 6 | from charlatan import utils 7 | -------------------------------------------------------------------------------- /charlatan/tests/data/cyclic_dependencies.yaml: -------------------------------------------------------------------------------- 1 | fixture1: 2 | depend_on: 3 | - fixture2 4 | 5 | fixture2: 6 | fields: 7 | a: !rel fixture3 8 | 9 | fixture3: 10 | fields: 11 | b: !rel fixture1 12 | -------------------------------------------------------------------------------- /docs/installation.rst: -------------------------------------------------------------------------------- 1 | Installation 2 | ============ 3 | 4 | From PyPI:: 5 | 6 | $ pip install charlatan 7 | 8 | From sources:: 9 | 10 | $ git clone https://github.com/uber/charlatan.git 11 | $ python setup.py install 12 | -------------------------------------------------------------------------------- /docs/examples/dependencies.yaml: -------------------------------------------------------------------------------- 1 | fixture1: 2 | fields: 3 | - name: "foo" 4 | 5 | fixture2: 6 | depend_on: 7 | - fixture1 8 | fields: 9 | - name: "bar" 10 | post_creation: 11 | - some_descriptor_that_depend_on_fixture1: true 12 | -------------------------------------------------------------------------------- /charlatan/tests/example/data/sqlalchemy.yaml: -------------------------------------------------------------------------------- 1 | toaster: 2 | fields: 3 | color: !rel color 4 | name: "toaster1" 5 | model: charlatan.tests.fixtures.models:Toaster 6 | 7 | color: 8 | fields: 9 | name: "red" 10 | model: charlatan.tests.fixtures.models:Color 11 | -------------------------------------------------------------------------------- /charlatan/tests/data/lists.yaml: -------------------------------------------------------------------------------- 1 | fixture_list: 2 | objects: 3 | - 4 | field1: stuff 5 | - 6 | field1: more_stuff 7 | 8 | related_fixture: 9 | fields: 10 | elements: !rel fixture_list 11 | 12 | list_fixture: 13 | fields: 14 | - Lolin 15 | - at 16 | - Stuff 17 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | # If you add a lib here, please include a short description of what it's doing 2 | 3 | # Documentation 4 | Sphinx==1.3b2 5 | # Releaser tool 6 | zest.releaser==5.5 7 | # Package checking 8 | pyroma==1.6 9 | # Check packages version 10 | pip-tools==0.3.4 11 | # Better pdb 12 | ipdb==0.8 13 | -------------------------------------------------------------------------------- /charlatan/tests/data/dependencies.yaml: -------------------------------------------------------------------------------- 1 | fixture1: 2 | fields: 3 | foo: bar 4 | 5 | fixture2: 6 | fields: 7 | foo: !rel fixture1 8 | 9 | fixture3: 10 | depend_on: 11 | - fixture1 12 | fields: 13 | foo: !rel fixture4 14 | 15 | fixture4: 16 | depend_on: 17 | - fixture2 18 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.7" 4 | - "3.5" 5 | 6 | install: 7 | - "pip install -r requirements.txt" 8 | - "pip install -r requirements-test.txt" 9 | - "python setup.py develop" 10 | 11 | script: flake8 charlatan && coverage run --source charlatan setup.py test 12 | 13 | after_success: coveralls 14 | -------------------------------------------------------------------------------- /charlatan/tests/data/special_tags.yaml: -------------------------------------------------------------------------------- 1 | current_time: !now 2 | 3 | current_naive_time: !now_naive 4 | 5 | tomorrow: !now +1d 6 | 7 | current_epoch_time: !epoch_now 8 | 9 | tomorrow_epoch_time: !epoch_now +1d 10 | 11 | current_epoch_time_in_ms: !epoch_now_in_ms 12 | 13 | tomorrow_epoch_time_in_ms: !epoch_now_in_ms +1d 14 | 15 | relationship: !rel tomorrow 16 | -------------------------------------------------------------------------------- /charlatan/tests/example/test_deep_inherit.py: -------------------------------------------------------------------------------- 1 | from charlatan import FixturesManager 2 | 3 | 4 | def test_deep_inherit(): 5 | manager = FixturesManager() 6 | manager.load('./charlatan/tests/example/data/deep_inherit.yaml') 7 | toaster2 = manager.get_fixture('toaster2') 8 | assert toaster2['toasts']['toast1']['price'] == 10 9 | assert toaster2['toasts']['toast1']['weight'] == 20 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[co] 2 | tags 3 | 4 | # Packages 5 | *.egg 6 | *.egg-info 7 | dist 8 | build 9 | eggs 10 | parts 11 | bin 12 | var 13 | sdist 14 | develop-eggs 15 | .installed.cfg 16 | 17 | # Installer logs 18 | pip-log.txt 19 | 20 | # Unit test / coverage reports 21 | .coverage 22 | .tox 23 | cover 24 | htmlcov 25 | 26 | # Translations 27 | *.mo 28 | 29 | # Doc 30 | docs/_build 31 | 32 | # pyenv 33 | .python-version 34 | -------------------------------------------------------------------------------- /charlatan/tests/fixtures/simple_models.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | class Toaster(object): 4 | 5 | def __init__(self, color, slots=2, content=None): 6 | self.color = color 7 | self.slots = slots 8 | self.content = content 9 | 10 | def __repr__(self): 11 | return "" % self.color 12 | 13 | 14 | class User(object): 15 | 16 | def __init__(self, toasters): 17 | self.toasters = toasters 18 | -------------------------------------------------------------------------------- /charlatan/tests/example/data/deep_inherit.yaml: -------------------------------------------------------------------------------- 1 | toaster: 2 | fields: 3 | toasts: 4 | toast1: 5 | type: brioche 6 | price: 10 7 | weight: 20 8 | 9 | toaster2: 10 | inherit_from: toaster 11 | deep_inherit: true 12 | fields: 13 | toasts: 14 | toast1: 15 | type: bread 16 | # Because of deep_inherit, the following fields are implied: 17 | # price: 10 18 | # weight: 20 19 | -------------------------------------------------------------------------------- /docs/database.rst: -------------------------------------------------------------------------------- 1 | Database fixtures 2 | ================= 3 | 4 | SQLAlchemy 5 | ---------- 6 | 7 | Charlatan has been heavily used and tested with sqlalchemy. Here's a simple 8 | example: 9 | 10 | Tests: 11 | 12 | .. literalinclude:: ../charlatan/tests/example/test_sqlalchemy.py 13 | 14 | YAML file: 15 | 16 | .. literalinclude:: ../charlatan/tests/example/data/sqlalchemy.yaml 17 | 18 | Model definition: 19 | 20 | .. literalinclude:: ../charlatan/tests/fixtures/models.py 21 | -------------------------------------------------------------------------------- /docs/api-reference.rst: -------------------------------------------------------------------------------- 1 | API Reference 2 | ============= 3 | 4 | FixturesManager 5 | --------------- 6 | 7 | .. autoclass:: charlatan.FixturesManager 8 | :members: 9 | 10 | 11 | FixturesManagerMixin 12 | -------------------- 13 | 14 | .. autoclass:: charlatan.FixturesManagerMixin 15 | :members: 16 | 17 | 18 | Fixture 19 | ------- 20 | 21 | .. autoclass:: charlatan.Fixture 22 | :members: 23 | 24 | 25 | Utils 26 | ----- 27 | 28 | .. automodule:: charlatan.utils 29 | :members: 30 | -------------------------------------------------------------------------------- /requirements-test.txt: -------------------------------------------------------------------------------- 1 | # If you add a lib here, please include a short description of what it's doing 2 | 3 | mock==1.0.1 4 | 5 | # Coverage 6 | coverage==3.7.1 7 | 8 | # Coverage service 9 | coveralls==0.4 10 | 11 | # Test runner 12 | pytest>=3,<4 13 | 14 | # Syntax checking 15 | flake8==2.4.1 16 | pyflakes==0.8.1 17 | pep8==1.5.7 18 | pep257==0.5.0 19 | flake8-docstrings==0.2.1.post1 20 | 21 | # Multi-version testing 22 | tox==1.6.1 23 | 24 | # Freeze time 25 | freezegun==0.2.8 26 | 27 | # For tests 28 | sqlalchemy==0.9.1 29 | schematics==1.0.2 30 | -------------------------------------------------------------------------------- /docs/examples/simple_fixtures.yaml: -------------------------------------------------------------------------------- 1 | toaster: # The fixture's name 2 | fields: # The fixture's content 3 | color: red 4 | slots: 5 5 | content: !rel toasts # You can reference other fixtures 6 | model: charlatan.tests.fixtures.simple_models:Toaster 7 | 8 | toaster_green: 9 | # Charlatan also supports inheritance 10 | inherit_from: toaster 11 | fields: 12 | color: green 13 | 14 | toasts: 15 | # No model is defined, so it defaults to what `fields` actually is, i.e. 16 | # in our case, a list. 17 | fields: 18 | - "Toast 1" 19 | - "Toast 2" 20 | -------------------------------------------------------------------------------- /docs/examples/fixtures.yaml: -------------------------------------------------------------------------------- 1 | toaster: 2 | fields: 3 | brand: Flying 4 | number_of_toasts: 2 5 | toasts: [!rel toast1, !rel toast2] 6 | bought: !now -1y 7 | model: Toaster 8 | 9 | toaster_already_in_db: 10 | id: 10 11 | model: Toaster 12 | 13 | toast1: 14 | fields: 15 | bread: brioche 16 | model: .toast:Toast 17 | 18 | toast2: 19 | fields: 20 | bread: campagne 21 | model: .toast:Toast 22 | 23 | user: 24 | fields: 25 | name: Michel Audiard 26 | toaster: !rel toaster 27 | model: toaster.models.user:User 28 | post_creation: 29 | has_used_toaster: true 30 | -------------------------------------------------------------------------------- /docs/examples/fixtures_inheritance.yaml: -------------------------------------------------------------------------------- 1 | first: 2 | fields: 3 | foo: bar 4 | 5 | # Everything is inherited from first. 6 | second: 7 | inherit_from: first 8 | 9 | # You can add fields without removing existing ones 10 | third: 11 | inherit_from: first 12 | fields: 13 | # foo: bar is implied by inheritance 14 | toaster: toasted 15 | 16 | # You can also overwrite the model. 17 | fourth: 18 | inherit_from: first 19 | model: collections:Counter 20 | 21 | # You can also overwrite both. 22 | fifth: 23 | inherit_from: second 24 | fields: 25 | toaster: toasted 26 | model: collections:Counter 27 | -------------------------------------------------------------------------------- /charlatan/tests/data/relationships_without_models.yaml: -------------------------------------------------------------------------------- 1 | simple_dict: 2 | fields: 3 | field1: lolin 4 | field2: 2 5 | 6 | dict_with_nest: 7 | fields: 8 | field1: asdlkf 9 | field2: 4 10 | simple_dict: !rel simple_dict 11 | 12 | list_of_relationships: 13 | fields: 14 | - !rel dict_with_nest 15 | - !rel simple_dict 16 | 17 | nested_list_of_relationships: 18 | fields: 19 | dicts: 20 | - 21 | - !rel dict_with_nest 22 | - 23 | - !rel simple_dict 24 | 25 | parent_dict: 26 | objects: 27 | object1: 28 | field1: 100 29 | 30 | child_dict: 31 | objects: 32 | object1: 33 | field1: !rel parent_dict.object1.field1 34 | -------------------------------------------------------------------------------- /charlatan/tests/data/relationships.yaml: -------------------------------------------------------------------------------- 1 | model: 2 | fields: 3 | color: !rel color 4 | name: "toaster1" 5 | model: charlatan.tests.fixtures.models:Toaster 6 | 7 | model_1: 8 | inherit_from: model 9 | fields: 10 | name: "toaster2" 11 | 12 | model_with_explicit_fk: 13 | model: charlatan.tests.fixtures.models:Toaster 14 | fields: 15 | name: 'toaster' 16 | color_id: !rel color.id 17 | 18 | color: 19 | fields: 20 | name: "red" 21 | model: charlatan.tests.fixtures.models:Color 22 | 23 | from_database: 24 | id: 1 25 | model: charlatan.tests.fixtures.models:Toaster 26 | 27 | model_list: 28 | inherit_from: model 29 | objects: 30 | - name: "one" 31 | - name: "two" 32 | -------------------------------------------------------------------------------- /docs/examples/relationships.yaml: -------------------------------------------------------------------------------- 1 | toaster: 2 | model: Toaster 3 | fields: 4 | color: red 5 | 6 | user: 7 | model: User 8 | fields: 9 | # You can link to another fixture 10 | toasters: 11 | - !rel toaster 12 | 13 | toaster_colors: 14 | # You can also link to a specific attribute 15 | fields: 16 | color: !rel toaster.color 17 | 18 | toaster_colors_list: 19 | fields: ['red'] 20 | 21 | # Let's define a collection 22 | toasters: 23 | model: Toaster 24 | objects: 25 | red: 26 | color: red 27 | 28 | toaster_from_collection: 29 | inherit_from: toaster 30 | fields: 31 | # You can link a specific attribute of a collection's item. 32 | color: !rel toasters.red.color 33 | -------------------------------------------------------------------------------- /docs/builders.rst: -------------------------------------------------------------------------------- 1 | .. _builders: 2 | 3 | Builders 4 | ======== 5 | 6 | Builders provide a powerful way to customize getting fixture. You can define 7 | your own builders and provide them as arguments when you instantiate 8 | :py:class:`charlatan.FixturesManager`. 9 | 10 | Example 11 | ------- 12 | 13 | Here's an example inspired by the schematics library, which expects a dict of 14 | attributes as a single instantiation argument: 15 | 16 | .. literalinclude:: ../charlatan/tests/example/test_custom_builder.py 17 | 18 | YAML file: 19 | 20 | .. literalinclude:: ../charlatan/tests/example/data/custom_builder.yaml 21 | 22 | 23 | API 24 | --- 25 | 26 | .. automodule:: charlatan.builder 27 | :members: 28 | :undoc-members: 29 | :special-members: __call__ 30 | -------------------------------------------------------------------------------- /charlatan/testing.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | import unittest 3 | 4 | 5 | class TestCase(unittest.TestCase): 6 | 7 | def __call__(self, result=None): 8 | """Run a test without having to call super in setUp and tearDown.""" 9 | self._pre_setup() 10 | 11 | unittest.TestCase.__call__(self, result) 12 | 13 | self._post_teardown() 14 | 15 | def _pre_setup(self): 16 | pass 17 | 18 | def _post_teardown(self): 19 | pass 20 | 21 | def assertCountEqual(self, *args, **kwargs): 22 | # Raaa Python 3 23 | try: 24 | return unittest.TestCase.assertCountEqual(self, *args, **kwargs) 25 | except AttributeError: 26 | return self.assertItemsEqual(*args, **kwargs) 27 | -------------------------------------------------------------------------------- /charlatan/tests/test_simple_testcase.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | import charlatan 4 | 5 | fixtures_manager = charlatan.FixturesManager() 6 | fixtures_manager.load("./docs/examples/simple_fixtures.yaml") 7 | 8 | 9 | class TestToaster(unittest.TestCase, charlatan.FixturesManagerMixin): 10 | 11 | def setUp(self): 12 | # Attach the fixtures manager to the instance 13 | self.fixtures_manager = fixtures_manager 14 | # Cleanup the cache 15 | self.init_fixtures() 16 | 17 | def test_example(self): 18 | """Verify that we can get fixtures.""" 19 | toaster = self.install_fixture("toaster") 20 | self.assertEqual(toaster.color, "red") 21 | self.assertEqual(toaster.slots, 5) 22 | self.assertEqual(toaster.content, ['Toast 1', 'Toast 2']) 23 | -------------------------------------------------------------------------------- /charlatan/tests/fixtures/models.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import create_engine 2 | from sqlalchemy import Column, Integer, String, ForeignKey 3 | from sqlalchemy.orm import sessionmaker, relationship 4 | from sqlalchemy.ext.declarative import declarative_base 5 | 6 | Base = declarative_base() 7 | engine = create_engine('sqlite:///:memory:') 8 | Session = sessionmaker(bind=engine) 9 | 10 | 11 | class Toaster(Base): 12 | 13 | __tablename__ = "toasters" 14 | 15 | id = Column(Integer, primary_key=True) 16 | name = Column(String) 17 | color_id = Column(String, ForeignKey('colors.name')) 18 | 19 | color = relationship("Color", backref='toasters') 20 | 21 | 22 | class Color(Base): 23 | 24 | __tablename__ = "colors" 25 | 26 | id = Column(Integer, primary_key=True) 27 | name = Column(String) 28 | -------------------------------------------------------------------------------- /charlatan/tests/example/test_custom_builder.py: -------------------------------------------------------------------------------- 1 | from charlatan import FixturesManager 2 | from charlatan.builder import Builder 3 | 4 | 5 | class Toaster(object): 6 | 7 | def __init__(self, attrs): 8 | self.slots = attrs['slots'] 9 | 10 | 11 | class DictBuilder(Builder): 12 | 13 | def __call__(self, fixtures, klass, params, **kwargs): 14 | # A "normal" object would be instantiated this way: 15 | # return klass(**params) 16 | 17 | # Yet schematics object expect a dict of attributes as only argument. 18 | # So we'll do: 19 | return klass(params) 20 | 21 | 22 | def test_custom_builder(): 23 | manager = FixturesManager(get_builder=DictBuilder()) 24 | manager.load('./charlatan/tests/example/data/custom_builder.yaml') 25 | assert manager.get_fixture('toaster').slots == 3 26 | -------------------------------------------------------------------------------- /charlatan/tests/test_fixture_collection.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from charlatan import FixturesManager 4 | 5 | 6 | def get_collection(collection): 7 | """Return FixtureCollection. 8 | 9 | :param str collection: name of collection to import 10 | """ 11 | manager = FixturesManager() 12 | manager.load("docs/examples/collection.yaml") 13 | return manager.collection.get(collection) 14 | 15 | 16 | @pytest.fixture(scope="module") 17 | def collection(): 18 | """Return FixtureCollection.""" 19 | return get_collection("toasters") 20 | 21 | 22 | def test_repr(collection): 23 | """Verify the repr.""" 24 | assert repr(collection) == "" 25 | 26 | 27 | def test_cant_get_missing_fixture(collection): 28 | """Verify that we can't get a missing fixture.""" 29 | with pytest.raises(KeyError): 30 | collection.get("missing") 31 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: bootstrap develop 2 | 3 | all: bootstrap develop test 4 | 5 | bootstrap: 6 | pip install -r requirements.txt 7 | pip install -r requirements-dev.txt 8 | pip install -r requirements-test.txt 9 | 10 | develop: 11 | python setup.py develop 12 | 13 | test: clean develop lint 14 | python setup.py test 15 | 16 | test-all: clean lint 17 | tox 18 | 19 | tu: 20 | py.test 21 | 22 | lint: 23 | flake8 charlatan 24 | 25 | coverage: 26 | coverage run --source charlatan setup.py test 27 | coverage report -m 28 | coverage html 29 | open htmlcov/index.html 30 | 31 | clean: clean-build 32 | find . -name '*.py[co]' -exec rm -f {} + 33 | 34 | clean-build: 35 | rm -fr build/ 36 | rm -fr dist/ 37 | rm -fr *.egg-info 38 | 39 | release: clean test docs 40 | fullrelease 41 | 42 | doc: clean develop 43 | $(MAKE) -C docs clean 44 | $(MAKE) -C docs html 45 | 46 | open_doc: doc 47 | open docs/_build/html/index.html 48 | -------------------------------------------------------------------------------- /charlatan/tests/example/test_sqlalchemy.py: -------------------------------------------------------------------------------- 1 | from charlatan import testing 2 | from charlatan import FixturesManager 3 | from charlatan.tests.fixtures.models import Session, Base, engine 4 | from charlatan.tests.fixtures.models import Toaster 5 | 6 | session = Session() 7 | manager = FixturesManager(db_session=session) 8 | manager.load("./charlatan/tests/example/data/sqlalchemy.yaml") 9 | 10 | 11 | class TestSqlalchemyFixtures(testing.TestCase): 12 | 13 | def setUp(self): 14 | self.manager = manager 15 | 16 | # There's a lot of different patterns to setup and teardown the 17 | # database. This is the simplest possibility. 18 | Base.metadata.create_all(engine) 19 | 20 | def tearDown(self): 21 | Base.metadata.drop_all(engine) 22 | session.close() 23 | 24 | def test_double_install(self): 25 | """Verify that there's no double install.""" 26 | self.manager.install_fixture('toaster') 27 | 28 | toaster = session.query(Toaster).one() 29 | assert toaster.color.name == 'red' 30 | -------------------------------------------------------------------------------- /charlatan/tests/test_utils.py: -------------------------------------------------------------------------------- 1 | from charlatan.utils import deep_update 2 | 3 | 4 | def test_deep_update(): 5 | source = {'hello1': 1} 6 | overrides = {'hello2': 2} 7 | deep_update(source, overrides) 8 | assert source == {'hello1': 1, 'hello2': 2} 9 | 10 | source = {'hello': 'to_override'} 11 | overrides = {'hello': 'over'} 12 | deep_update(source, overrides) 13 | assert source == {'hello': 'over'} 14 | 15 | source = {'hello': {'value': 'to_override', 'no_change': 1}} 16 | overrides = {'hello': {'value': 'over'}} 17 | deep_update(source, overrides) 18 | assert source == {'hello': {'value': 'over', 'no_change': 1}} 19 | 20 | source = {'hello': {'value': 'to_override', 'no_change': 1}} 21 | overrides = {'hello': {'value': {}}} 22 | deep_update(source, overrides) 23 | assert source == {'hello': {'value': {}, 'no_change': 1}} 24 | 25 | source = {'hello': {'value': {}, 'no_change': 1}} 26 | overrides = {'hello': {'value': 2}} 27 | deep_update(source, overrides) 28 | assert source == {'hello': {'value': 2, 'no_change': 1}} 29 | -------------------------------------------------------------------------------- /charlatan/tests/test_richgetter.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | from charlatan import testing 4 | from charlatan.utils import richgetter 5 | 6 | 7 | class TestRichGetter(testing.TestCase): 8 | def test_resolves_objects(self): 9 | obj = Namespace(key=Namespace(value='value')) 10 | 11 | result = richgetter(obj, 'key.value') 12 | 13 | self.assertEquals(result, 'value') 14 | 15 | def test_resolves_mappings(self): 16 | obj = dict(key=dict(value='value')) 17 | 18 | result = richgetter(obj, 'key.value') 19 | 20 | self.assertEquals(result, 'value') 21 | 22 | def test_resolves_sequences(self): 23 | obj = [['value']] 24 | 25 | result = richgetter(obj, '0.0') 26 | 27 | self.assertEquals(result, 'value') 28 | 29 | def test_resolves_mixed_containers(self): 30 | obj = Namespace(key=dict(values=['value'])) 31 | 32 | result = richgetter(obj, 'key.values.0') 33 | 34 | self.assertEquals(result, 'value') 35 | 36 | 37 | class Namespace(object): 38 | def __init__(self, **kwargs): 39 | for key, value in kwargs.items(): 40 | setattr(self, key, value) 41 | -------------------------------------------------------------------------------- /charlatan/tests/test_lists_of_fixtures.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | import operator as op 3 | 4 | from charlatan import testing 5 | from charlatan import FixturesManager 6 | 7 | 8 | class TestListOfFixtures(testing.TestCase): 9 | 10 | def setUp(self): 11 | self.fm = FixturesManager() 12 | self.fm.load('./charlatan/tests/data/lists.yaml') 13 | 14 | def test_get_list_by_name(self): 15 | """Verify that lists of fixtures returns lists.""" 16 | fixtures = self.fm.install_fixture('fixture_list') 17 | self.assertIsInstance(fixtures, list) 18 | 19 | def test_one_to_many_relationship(self): 20 | """Verify that relations to lists of fixtures work.""" 21 | fixture = self.fm.install_fixture('related_fixture') 22 | self.assertEqual( 23 | fixture['elements'], 24 | self.fm.install_fixture('fixture_list') 25 | ) 26 | 27 | def test_override(self): 28 | """Verify that we can override attributes on a list of fixtures.""" 29 | fixtures = self.fm.install_fixture('fixture_list', 30 | overrides={"field1": 12}) 31 | assert list(map(op.itemgetter('field1'), fixtures)) == [12, 12] 32 | -------------------------------------------------------------------------------- /docs/examples/collection.yaml: -------------------------------------------------------------------------------- 1 | toasters: 2 | model: charlatan.tests.fixtures.simple_models:Toaster 3 | 4 | # Those are the default for all fixtures 5 | fields: 6 | slots: 5 7 | 8 | # You can have named fixtures in the collection. Note the use of dict. 9 | objects: 10 | green: # This fixture can be accessed via toaster.green 11 | color: green 12 | blue: 13 | color: blue 14 | 15 | anonymous_toasters: 16 | inherit_from: toasters 17 | 18 | # Here we define unamed fixtures. Note that we use a list instead of a dict. 19 | objects: 20 | # You access the first fixture via anonymous_toaster.0 21 | - 22 | color: yellow 23 | - 24 | color: black 25 | 26 | # Those collections can be used as is in relationships. 27 | 28 | collection: 29 | fields: 30 | # Since we defined the toasters collection as a dict, things's value will 31 | # be a dict as well 32 | things: !rel toasters 33 | 34 | users: 35 | model: charlatan.tests.fixtures.simple_models:User 36 | 37 | objects: 38 | 39 | 1: 40 | toasters: !rel anonymous_toasters 41 | 42 | 2: 43 | # You can also link to specific relationships using the namespace 44 | toasters: [!rel toasters.green] 45 | 46 | 3: 47 | toasters: [!rel anonymous_toasters.0] 48 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Charlatan: Fixtures Made Easy [unmaintained] 2 | ============================================ 3 | 4 | **Efficiently manage and install data fixtures** 5 | 6 | .. image:: https://travis-ci.org/uber/charlatan.png?branch=master 7 | :target: https://travis-ci.org/uber/charlatan 8 | 9 | .. image:: https://coveralls.io/repos/uber/charlatan/badge.png 10 | :target: https://coveralls.io/r/uber/charlatan 11 | 12 | **⚠️ This repository is not actively maintained anymore.** 13 | 14 | `Charlatan` is a library that you can use in your tests to create database 15 | fixtures. Its aim is to provide a pragmatic interface that focuses on making it 16 | simple to define and install fixtures for your tests. It is also agnostic in so 17 | far as even though it's designed to work out of the box with SQLAlchemy models, 18 | it can work with pretty much anything else. 19 | 20 | Documentation 21 | ------------- 22 | 23 | Latest documentation: 24 | `charlatan.readthedocs.org/en/latest/ `_ 25 | 26 | Installation 27 | ------------ 28 | 29 | Using `pip`:: 30 | 31 | $ pip install charlatan 32 | 33 | License 34 | ------- 35 | 36 | charlatan is available under the MIT License. 37 | 38 | Copyright Uber 2013-2017, Charles-Axel Dein 39 | 40 | Authors 41 | ------- 42 | 43 | - Charles-Axel Dein 44 | - Erik Formella 45 | -------------------------------------------------------------------------------- /charlatan/tests/test_relationships_without_models.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | from charlatan import testcase, testing, FixturesManager 4 | 5 | 6 | class TestRelationshipsWithoutModels(testing.TestCase, 7 | testcase.FixturesManagerMixin): 8 | 9 | def setUp(self): 10 | self.fixtures_manager = FixturesManager() 11 | self.fixtures_manager.load( 12 | './charlatan/tests/data/relationships_without_models.yaml') 13 | self.install_fixtures([ 14 | 'dict_with_nest', 'simple_dict', 'list_of_relationships']) 15 | self.init_fixtures() 16 | 17 | def test_dictionaries_nest(self): 18 | self.assertEqual(self.dict_with_nest['simple_dict'], self.simple_dict) 19 | 20 | def test_relationships_list(self): 21 | self.assertEqual([self.dict_with_nest, self.simple_dict], 22 | self.list_of_relationships) 23 | 24 | def test_nested_list_of_relationships(self): 25 | nested_list_of_relationships = self.install_fixture( 26 | 'nested_list_of_relationships') 27 | 28 | self.assertEqual(nested_list_of_relationships, { 29 | 'dicts': [ 30 | [self.dict_with_nest], 31 | [self.simple_dict], 32 | ] 33 | }) 34 | 35 | def test_relationships_dict_attribute(self): 36 | parent = self.install_fixture('parent_dict.object1') 37 | child = self.install_fixture('child_dict.object1') 38 | 39 | self.assertEquals(child['field1'], parent['field1']) 40 | -------------------------------------------------------------------------------- /docs/hooks.rst: -------------------------------------------------------------------------------- 1 | .. _hooks: 2 | 3 | Hooks 4 | ===== 5 | 6 | 7 | The following hooks are available: 8 | 9 | * ``before_install``: called before doing anything. The callback takes no 10 | argument. 11 | * ``before_save``: called before saving an instance using the SQLAlchemy 12 | session. The callback takes a single argument which is the instance being 13 | saved. 14 | * ``after_save``: called after saving an instance using the SQLAlchemy session. 15 | The callback takes a single argument which is the instance that was saved. 16 | * ``after_install``: called after doing anything. The callback must accept a 17 | single argument that will be the exception that may have been raised during 18 | the whole process. This function is guaranteed to be called. 19 | * ``before_uninstall``: called before uninstalling fixtures. The callback takes 20 | no argument. 21 | * ``before_delete``: called before deleting an instance using either the 22 | SQLAlchemy session or in the following order `delete_instance` and `delete`. 23 | The callback takes a single argument which is the instance being deleted. 24 | * ``after_delete``: called after deleting an instance using either the 25 | SQLAlchemy session or in the following order `delete_instance` and `delete`. 26 | The callback takes a single argument which is the instance that was deleted. 27 | * ``after_uninstall``: called after uninstalling fixtures. The callback must 28 | accept a single argument that will be the exception that may have been raised 29 | during the whole process. This function is guaranteed to be called. 30 | 31 | .. automethod:: charlatan.FixturesManager.set_hook 32 | :noindex: 33 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. charlatan documentation master file, created by 2 | sphinx-quickstart on Wed Feb 6 11:21:22 2013. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to Charlatan's documentation! [unmaintained] 7 | ==================================================== 8 | 9 | .. WARNING:: 10 | This repository is not actively maintained anymore. 11 | 12 | Charlatan is a library that lets you efficiently manage and install 13 | fixtures. 14 | 15 | Its features include: 16 | 17 | - Straightforward YAML syntax to define fixtures. 18 | - Rich fixture definition functionalities, including inheritance and 19 | relationships (fixtures factory). 20 | - ORM-agnostic. Tested with sqlalchemy, schematics, etc. 21 | - Flexible thanks to :ref:`hooks` or :ref:`builders`. 22 | 23 | Charlatan is a library that you can use in your tests to create database 24 | fixtures. Its aim is to provide a pragmatic interface that focuses on making it 25 | simple to define and install fixtures for your tests. 26 | 27 | Charlatan supports Python 2 (only tested with 2.7) and 3 (tested with 3.3). 28 | 29 | **Why Charlatan?** Since "charlatan" used to define "an itinerant seller of 30 | supposed remedies", we thought it would be a good name for a library providing 31 | fixtures for tests. Credit for the name goes to Zack Heller. 32 | 33 | Contents 34 | -------- 35 | 36 | .. toctree:: 37 | :maxdepth: 2 38 | 39 | installation 40 | quickstart 41 | file-format 42 | database 43 | hooks 44 | builders 45 | api-reference 46 | contributing 47 | changelog 48 | 49 | 50 | Indices and tables 51 | ================== 52 | 53 | * :ref:`genindex` 54 | * :ref:`modindex` 55 | * :ref:`search` 56 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import sys 3 | 4 | from setuptools import setup 5 | from setuptools.command.test import test as TestCommand 6 | 7 | 8 | def read_long_description(filename="README.rst"): 9 | with open(filename) as f: 10 | return f.read().strip() 11 | 12 | 13 | def read_requirements(filename="requirements.txt"): 14 | with open(filename) as f: 15 | return f.readlines() 16 | 17 | 18 | class PyTest(TestCommand): 19 | def finalize_options(self): 20 | TestCommand.finalize_options(self) 21 | self.test_args = [] 22 | self.test_suite = True 23 | 24 | def run_tests(self): 25 | import pytest 26 | errcode = pytest.main(self.test_args) 27 | sys.exit(errcode) 28 | 29 | setup( 30 | name="charlatan", 31 | version='0.4.8.dev0', 32 | author="Charles-Axel Dein", 33 | author_email="charles@uber.com", 34 | license="MIT", 35 | tests_require=['pytest'], 36 | cmdclass={'test': PyTest}, 37 | url="https://github.com/uber/charlatan", 38 | packages=["charlatan"], 39 | keywords=["tests", "fixtures", "database"], 40 | description="Efficiently manage and install data fixtures", 41 | long_description=read_long_description(), 42 | install_requires=["PyYAML>=3.10", "pytz"], 43 | zip_safe=False, 44 | classifiers=[ 45 | "Development Status :: 4 - Beta", 46 | "Programming Language :: Python", 47 | "Programming Language :: Python :: 2.7", 48 | "Programming Language :: Python :: 3", 49 | "Programming Language :: Python :: 3.3", 50 | "Intended Audience :: Developers", 51 | "License :: OSI Approved :: MIT License", 52 | "Operating System :: OS Independent", 53 | "Topic :: Software Development :: Testing", 54 | "Topic :: Software Development :: Libraries :: Python Modules" 55 | ] 56 | ) 57 | -------------------------------------------------------------------------------- /charlatan/_compat.py: -------------------------------------------------------------------------------- 1 | """Utilities for writing code that runs on Python 2 and 3.""" 2 | 3 | # Copyright (c) 2010-2013 Benjamin Peterson 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining 6 | # a copy of this software and associated documentation files (the 7 | # "Software"), to deal in the Software without restriction, including 8 | # without limitation the rights to use, copy, modify, merge, publish, 9 | # distribute, sublicense, and/or sell copies of the Software, and to 10 | # permit persons to whom the Software is furnished to do so, subject to 11 | # the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be 14 | # included in all copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | # CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | import sys 25 | 26 | # Useful for very coarse version differentiation. 27 | PY2 = sys.version_info[0] == 2 28 | PY3 = sys.version_info[0] == 3 29 | 30 | if PY3: 31 | string_types = str, 32 | 33 | else: 34 | string_types = basestring, # noqa 35 | 36 | 37 | if PY3: 38 | _iteritems = "items" 39 | _itervalues = "values" 40 | 41 | else: 42 | _iteritems = "iteritems" 43 | _itervalues = "itervalues" 44 | 45 | 46 | def iteritems(d, **kw): 47 | """Return an iterator over the (key, value) pairs of a dictionary.""" 48 | return iter(getattr(d, _iteritems)(**kw)) 49 | 50 | 51 | def itervalues(d, **kw): 52 | """Return an iterator over the values of a dictionary.""" 53 | return iter(getattr(d, _itervalues)(**kw)) 54 | -------------------------------------------------------------------------------- /charlatan/tests/test_depgraph.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | from charlatan import testing 4 | from charlatan.depgraph import DepGraph, HasACycle 5 | 6 | 7 | class TestDepGraph(testing.TestCase): 8 | 9 | def test_topo_sort(self): 10 | """Test the topo_sort function of DepGraph.""" 11 | d = DepGraph() 12 | # 13 | # a b 14 | # \ / 15 | # c 16 | # | 17 | # d 18 | # / \ 19 | # e f 20 | d.add_edge('a', 'c') 21 | d.add_edge('b', 'c') 22 | d.add_edge('c', 'd') 23 | d.add_edge('d', 'e') 24 | d.add_edge('d', 'f') 25 | l = d.topo_sort() 26 | self.assertCountEqual(l, ['a', 'b', 'c', 'd', 'e', 'f']) 27 | assert d.acyclic 28 | 29 | def test_topo_sort_knows_what_cycles_are(self): 30 | """Test that topo_sort fails on cyclic graphs.""" 31 | d = DepGraph() 32 | d.add_edge('a', 'b') 33 | d.add_edge('b', 'c') 34 | d.add_edge('c', 'a') 35 | self.assertRaises(HasACycle, d.topo_sort) 36 | assert not d.acyclic 37 | 38 | def test_ancestors_of(self): 39 | """Test the ancestors_of function in DepGraph.""" 40 | d = DepGraph() 41 | # 42 | # a b 43 | # \ / 44 | # c 45 | # / \ 46 | # d e 47 | # | 48 | # f 49 | d.add_edge('a', 'c') 50 | d.add_edge('b', 'c') 51 | d.add_edge('c', 'd') 52 | d.add_edge('c', 'e') 53 | d.add_edge('d', 'f') 54 | l = d.ancestors_of('d') 55 | self.assertCountEqual(l, ['a', 'b', 'c']) 56 | 57 | def test_has_edge_between(self): 58 | """Test the has_edge_between function.""" 59 | d = DepGraph() 60 | # 61 | # a b 62 | # | | 63 | # c -- d e 64 | d.add_edge('a', 'c') 65 | d.add_edge('b', 'd') 66 | d.add_edge('c', 'd') 67 | d.add_node('e') 68 | assert d.has_edge_between('a', 'c') 69 | assert not d.has_edge_between('c', 'a'), 'should not be commutative' 70 | assert not d.has_edge_between('a', 'b'), 'should be edges, not paths' 71 | assert not d.has_edge_between('e', 'd') 72 | -------------------------------------------------------------------------------- /charlatan/tests/test_sqlalchemy.py: -------------------------------------------------------------------------------- 1 | from charlatan import testing 2 | from charlatan import FixturesManager 3 | from charlatan.tests.fixtures.models import Session, Base, engine 4 | from charlatan.tests.fixtures.models import Toaster, Color 5 | 6 | 7 | class TestSqlalchemyFixtures(testing.TestCase): 8 | 9 | def setUp(self): 10 | self.session = Session() 11 | self.manager = FixturesManager(db_session=self.session) 12 | self.manager.load("./charlatan/tests/data/relationships.yaml") 13 | 14 | Base.metadata.create_all(engine) 15 | 16 | def tearDown(self): 17 | Base.metadata.drop_all(engine) 18 | self.session.close() 19 | 20 | def test_double_install(self): 21 | """Verify that there's no double install.""" 22 | self.manager.install_fixture("model") 23 | self.manager.install_fixture("color") 24 | 25 | self.assertEqual(self.session.query(Toaster).count(), 1) 26 | self.assertEqual(self.session.query(Color).count(), 1) 27 | 28 | def test_getting_from_database(self): 29 | """Verify that we can get from the database.""" 30 | installed = Toaster(id=1) 31 | self.session.add(installed) 32 | self.session.commit() 33 | 34 | toaster = self.manager.install_fixture("from_database") 35 | self.assertEqual(toaster.id, 1) 36 | 37 | def test_installing_collection(self): 38 | """Verify that a collection of fixtures is in the database.""" 39 | self.manager.install_fixture("model_list") 40 | 41 | self.assertEqual(self.session.query(Toaster).count(), 2) 42 | 43 | def test_inheritance_and_relationship(self): 44 | """Verify that inheritance works with relationships.""" 45 | model, model_1 = self.manager.install_fixtures(('model', 'model_1')) 46 | 47 | self.assertTrue(isinstance(model.color, Color)) 48 | self.assertTrue(isinstance(model_1.color, Color)) 49 | 50 | def test_explicit_foreign_key(self): 51 | """Verify that we can get a db-computed foreign key explicitely.""" 52 | model = self.manager.install_fixture('model_with_explicit_fk') 53 | assert model.color_id is not None 54 | 55 | def test_uninstall_deletes_fixtures(self): 56 | """Verify uninstalling a fixture drops it from the database.""" 57 | self.manager.install_fixture("color") 58 | 59 | # sanity check 60 | self.assertEqual(self.session.query(Color).count(), 1) 61 | 62 | self.manager.uninstall_fixture("color") 63 | 64 | self.assertEqual(self.session.query(Color).count(), 0) 65 | -------------------------------------------------------------------------------- /charlatan/testcase.py: -------------------------------------------------------------------------------- 1 | from charlatan.utils import copy_docstring_from 2 | from charlatan import FixturesManager 3 | from charlatan.fixtures_manager import make_list 4 | 5 | 6 | class FixturesManagerMixin(object): 7 | 8 | """Class from which test cases should inherit to use fixtures. 9 | 10 | .. versionchanged:: 0.3.12 11 | ``FixturesManagerMixin`` does not install class attributes 12 | ``fixtures`` anymore. 13 | 14 | .. versionchanged:: 0.3.0 15 | ``use_fixtures_manager`` method renamed ``init_fixtures.`` 16 | 17 | .. versionchanged:: 0.3.0 18 | Extensive change to the function signatures. 19 | 20 | """ 21 | 22 | def init_fixtures(self): 23 | """Initialize the fixtures. 24 | 25 | This function *must* be called before doing anything else. 26 | """ 27 | self.fixtures_manager.clean_cache() 28 | 29 | @copy_docstring_from(FixturesManager) 30 | def install_fixture(self, fixture_key, overrides=None): 31 | fixture = self.fixtures_manager.install_fixture(fixture_key, overrides) 32 | setattr(self, fixture_key, fixture) 33 | return fixture 34 | 35 | @copy_docstring_from(FixturesManager) 36 | def install_fixtures(self, fixtures): 37 | installed = [] 38 | for fixture in make_list(fixtures): 39 | installed.append( 40 | self.install_fixture(fixture) 41 | ) 42 | 43 | return installed 44 | 45 | @copy_docstring_from(FixturesManager) 46 | def install_all_fixtures(self): 47 | return self.install_fixtures(self.fixtures_manager.keys()) 48 | 49 | @copy_docstring_from(FixturesManager) 50 | def get_fixture(self, fixture_key, overrides=None): 51 | return self.fixtures_manager.get_fixture(fixture_key, overrides) 52 | 53 | @copy_docstring_from(FixturesManager) 54 | def get_fixtures(self, fixtures, builder=None): 55 | return self.fixtures_manager.get_fixtures(fixtures, builder=builder) 56 | 57 | @copy_docstring_from(FixturesManager) 58 | def uninstall_fixture(self, fixture_key): 59 | self.fixtures_manager.uninstall_fixture(fixture_key) 60 | 61 | @copy_docstring_from(FixturesManager) 62 | def uninstall_fixtures(self, fixtures): 63 | for fixture in make_list(fixtures): 64 | self.uninstall_fixture(fixture) 65 | 66 | @copy_docstring_from(FixturesManager) 67 | def uninstall_all_fixtures(self): 68 | # copy and reverse the list in order to remove objects with 69 | # relationships first 70 | installed_fixtures = list(self.fixtures_manager.installed_keys) 71 | installed_fixtures.reverse() 72 | return self.uninstall_fixtures(installed_fixtures) 73 | -------------------------------------------------------------------------------- /charlatan/depgraph.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from __future__ import print_function 3 | 4 | import collections 5 | import copy 6 | 7 | from ._compat import itervalues 8 | 9 | 10 | class HasACycle(Exception): 11 | pass 12 | 13 | 14 | class DepGraph(object): 15 | 16 | """A simple directed graph, suitable for doing dependency management.""" 17 | 18 | def __init__(self): 19 | self.nodes = set([]) 20 | self.rtl_edges = collections.defaultdict(list) 21 | self.ltr_edges = collections.defaultdict(list) 22 | self._topo_sort_cache = None 23 | self.dirty = True 24 | 25 | def has_edge_between(self, lhs, rhs): 26 | return rhs in self.ltr_edges[lhs] 27 | 28 | def add_node(self, node): 29 | self.nodes.add(node) 30 | self.dirty = True 31 | 32 | def add_edge(self, lhs, rhs): 33 | self.add_node(lhs) 34 | self.add_node(rhs) 35 | self.rtl_edges[rhs].append(lhs) 36 | self.ltr_edges[lhs].append(rhs) 37 | self.dirty = True 38 | 39 | @property 40 | def acyclic(self): 41 | try: 42 | self.topo_sort() 43 | except HasACycle: 44 | return False 45 | return True 46 | 47 | def _topo_sort(self): 48 | root_nodes = [] 49 | for node in self.nodes: 50 | if not self.rtl_edges[node]: 51 | root_nodes.append(node) 52 | sorted_list = [] 53 | edges = copy.deepcopy(self.ltr_edges) 54 | back_edges = copy.deepcopy(self.rtl_edges) 55 | while root_nodes: 56 | node = root_nodes.pop(0) 57 | sorted_list.append(node) 58 | for target in edges[node][:]: 59 | edges[node].remove(target) 60 | back_edges[target].remove(node) 61 | if not back_edges[target]: 62 | root_nodes.append(target) 63 | if any(v for v in itervalues(edges)): 64 | raise HasACycle() 65 | return sorted_list 66 | 67 | def topo_sort(self): 68 | if not self._topo_sort_cache or self.dirty: 69 | self._topo_sort_cache = self._topo_sort() 70 | self.dirty = False 71 | return self._topo_sort_cache 72 | 73 | def ancestors_of(self, node): 74 | """Return a list of ancestors of given node, in topological order.""" 75 | parents = [] 76 | work_queue = [node] 77 | while work_queue: 78 | node = work_queue.pop(0) 79 | for parent in self.rtl_edges[node]: 80 | parents.append(parent) 81 | work_queue.append(parent) 82 | topo_order = self.topo_sort() 83 | return sorted(parents, key=lambda i: topo_order.index(i)) 84 | -------------------------------------------------------------------------------- /charlatan/builder.py: -------------------------------------------------------------------------------- 1 | from charlatan.utils import is_sqlalchemy_model 2 | 3 | 4 | class Builder(object): 5 | 6 | def __call__(self, fixtures, klass, params, **kwargs): 7 | """Build a fixture. 8 | 9 | :param FixturesManager fixtures: 10 | :param klass: the fixture's class (``model`` in the definition file) 11 | :param params: the fixture's params (``fields`` in the definition 12 | file) 13 | :param dict kwargs: 14 | 15 | ``kwargs`` allows passing arguments to the builder to change its 16 | behavior. 17 | """ 18 | raise NotImplementedError 19 | 20 | 21 | class InstantiateAndSave(Builder): 22 | 23 | def __call__(self, fixtures, klass, params, **kwargs): 24 | """Save a fixture instance. 25 | 26 | If it's a SQLAlchemy model, it will be added to the session and 27 | the session will be committed. 28 | 29 | Otherwise, a :meth:`save` method will be run if the instance has 30 | one. If it does not have one, nothing will happen. 31 | 32 | Before and after the process, the :func:`before_save` and 33 | :func:`after_save` hook are run. 34 | 35 | """ 36 | session = kwargs.get('session') 37 | save = kwargs.get('save') 38 | 39 | instance = self.instantiate(klass, params) 40 | if save: 41 | self.save(instance, fixtures, session) 42 | return instance 43 | 44 | def instantiate(self, klass, params): 45 | """Return instantiated instance.""" 46 | try: 47 | return klass(**params) 48 | except TypeError as exc: 49 | raise TypeError("Error while trying to build %r " 50 | "with %r: %s" % (klass, params, exc)) 51 | 52 | def save(self, instance, fixtures, session): 53 | """Save instance.""" 54 | fixtures.get_hook("before_save")(instance) 55 | 56 | if session and is_sqlalchemy_model(instance): 57 | session.add(instance) 58 | session.commit() 59 | 60 | else: 61 | getattr(instance, "save", lambda: None)() 62 | 63 | fixtures.get_hook("after_save")(instance) 64 | 65 | 66 | class DeleteAndCommit(Builder): 67 | 68 | def __call__(self, fixtures, instance, **kwargs): 69 | session = kwargs.get('session') 70 | commit = kwargs.get('commit') 71 | 72 | fixtures.get_hook("before_uninstall")() 73 | try: 74 | if commit: 75 | self.delete(instance, session) 76 | else: 77 | try: 78 | getattr(instance, "delete_instance")() 79 | except AttributeError: 80 | getattr(instance, "delete", lambda: None)() 81 | 82 | except Exception as exc: 83 | fixtures.get_hook("after_uninstall")(exc) 84 | raise 85 | 86 | else: 87 | fixtures.get_hook("after_uninstall")(None) 88 | 89 | def delete(self, instance, session): 90 | """Delete instance.""" 91 | if session and is_sqlalchemy_model(instance): 92 | session.delete(instance) 93 | session.commit() 94 | -------------------------------------------------------------------------------- /charlatan/tests/test_testcase.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | import mock 4 | 5 | from charlatan import testcase 6 | from charlatan import testing 7 | from charlatan import FixturesManager 8 | from charlatan.builder import Builder 9 | 10 | 11 | fixtures_manager = FixturesManager() 12 | fixtures_manager.load( 13 | './charlatan/tests/data/relationships_without_models.yaml') 14 | 15 | 16 | class TestTestCase(testing.TestCase, testcase.FixturesManagerMixin): 17 | 18 | def _pre_setup(self): 19 | self.fixtures_manager = fixtures_manager 20 | self.init_fixtures() 21 | self.install_fixtures(('simple_dict', 'dict_with_nest')) 22 | 23 | def _post_teardown(self): 24 | self.uninstall_all_fixtures() 25 | 26 | def test_install_fixture(self): 27 | """Verify install_fixture should return the installed fixture.""" 28 | self.uninstall_all_fixtures() 29 | 30 | simple_dict = self.install_fixture('simple_dict') 31 | self.assertEqual(simple_dict['field1'], 'lolin') 32 | self.assertEqual(simple_dict['field2'], 2) 33 | 34 | def test_install_fixtures(self): 35 | """Verify install_fixtures should return installed fixtures.""" 36 | self.uninstall_all_fixtures() 37 | 38 | fixtures = self.install_fixtures(('simple_dict', 'dict_with_nest')) 39 | self.assertEqual(len(fixtures), 2) 40 | 41 | def test_install_all_fixtures(self): 42 | """Verify it installs all fixtures of the yaml file.""" 43 | self.uninstall_all_fixtures() 44 | 45 | fixtures = self.install_all_fixtures() 46 | self.assertEqual(len(fixtures), 6) 47 | 48 | def test_uninstall_fixture(self): 49 | self.uninstall_fixture('simple_dict') 50 | self.uninstall_fixture('dict_with_nest') 51 | self.uninstall_all_fixtures() 52 | 53 | def test_uninstall_fixtures(self): 54 | self.uninstall_fixtures(('simple_dict', 'dict_with_nest')) 55 | self.uninstall_fixtures(('simple_dict', 'dict_with_nest')) 56 | 57 | def test_uninstall_all_fixtures(self): 58 | """Verify should uninstall all the installed fixtures. 59 | 60 | The _pre_setup method install the 2 fixtures defined in self.fixtures: 61 | 'simple_dict' and 'dict_with_nest'. 62 | """ 63 | self.uninstall_all_fixtures() 64 | 65 | def test_get_fixture(self): 66 | """Verify get_fixture should return the fixture.""" 67 | simple_dict = self.get_fixture('simple_dict') 68 | self.assertEqual(simple_dict['field1'], 'lolin') 69 | self.assertEqual(simple_dict['field2'], 2) 70 | 71 | dict_with_nest = self.get_fixture('dict_with_nest') 72 | self.assertEqual(dict_with_nest['field1'], 'asdlkf') 73 | self.assertEqual(dict_with_nest['field2'], 4) 74 | 75 | def test_get_fixtures(self): 76 | """Verify get_fixtures should return the list of fixtures.""" 77 | fixtures = self.get_fixtures(('simple_dict', 'dict_with_nest')) 78 | self.assertEqual(len(fixtures), 2) 79 | 80 | simple_dict = fixtures[0] 81 | self.assertEqual(simple_dict['field1'], 'lolin') 82 | self.assertEqual(simple_dict['field2'], 2) 83 | 84 | dict_with_nest = fixtures[1] 85 | self.assertEqual(dict_with_nest['field1'], 'asdlkf') 86 | self.assertEqual(dict_with_nest['field2'], 4) 87 | 88 | @mock.patch('charlatan.FixturesManager.get_fixture') 89 | def test_get_fixtures_builder(self, mocked_get_fixture): 90 | """Verify builder instance passed to inner get_fixture call.""" 91 | builder = Builder() 92 | self.get_fixtures(('simple_dict', 'dict_with_nest'), builder=builder) 93 | 94 | self.assertEqual(mocked_get_fixture.call_count, 2) 95 | mocked_get_fixture.assert_any_call('simple_dict', builder=builder) 96 | mocked_get_fixture.assert_any_call('dict_with_nest', builder=builder) 97 | -------------------------------------------------------------------------------- /charlatan/file_format.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | import datetime 3 | 4 | import pytz 5 | import yaml 6 | from yaml.constructor import Constructor 7 | 8 | from charlatan.utils import datetime_to_epoch_in_ms 9 | from charlatan.utils import datetime_to_epoch_timestamp 10 | from charlatan.utils import get_timedelta 11 | 12 | 13 | TIMEZONE_AWARE = True 14 | 15 | 16 | class RelationshipToken(str): 17 | 18 | """Class used to mark relationships. 19 | 20 | This token is used to mark relationships found in YAML file, so that they 21 | can be processed later. 22 | """ 23 | 24 | pass 25 | 26 | 27 | class UnnamedRelationshipToken(dict): 28 | 29 | """Class used to mark unamed relationships. 30 | 31 | This token is used to mark relationships found in YAML file, so that they 32 | can be processed later. 33 | """ 34 | 35 | pass 36 | 37 | 38 | def configure_yaml(): 39 | """Add some custom tags to the YAML constructor.""" 40 | def now_constructor(loader, node): 41 | """Return a function that returns the current datetime.""" 42 | delta = get_timedelta(loader.construct_scalar(node)) 43 | 44 | def get_now(): 45 | returned = datetime.datetime.utcnow() 46 | if TIMEZONE_AWARE: 47 | returned = returned.replace(tzinfo=pytz.utc) 48 | return returned + delta 49 | 50 | return get_now 51 | 52 | def now_naive_constructor(loader, node): 53 | """Return a function that returns the current datetime. 54 | 55 | The returned datetime is always a naive datetime (i.e. without 56 | timezone information). 57 | 58 | See the introduction in `datetime 59 | `_ for more 60 | information. 61 | """ 62 | delta = get_timedelta(loader.construct_scalar(node)) 63 | 64 | def get_now(): 65 | return datetime.datetime.utcnow() + delta 66 | 67 | return get_now 68 | 69 | def epoch_now_constructor(loader, node): 70 | """Return a function that returns the current epoch.""" 71 | delta = get_timedelta(loader.construct_scalar(node)) 72 | 73 | def get_now(): 74 | return datetime_to_epoch_timestamp( 75 | datetime.datetime.utcnow() + delta) 76 | 77 | return get_now 78 | 79 | def epoch_now_in_ms_constructor(loader, node): 80 | """Return a function that returns the current epoch in milliseconds. 81 | 82 | :rtype: int 83 | """ 84 | delta = get_timedelta(loader.construct_scalar(node)) 85 | 86 | def get_now(): 87 | return datetime_to_epoch_in_ms(datetime.datetime.utcnow() + delta) 88 | 89 | return get_now 90 | 91 | def relationship_constructor(loader, node): 92 | """Create _RelationshipToken for `!rel` tags.""" 93 | name = loader.construct_scalar(node) 94 | return RelationshipToken(name) 95 | 96 | yaml.add_constructor( 97 | u'!now', now_constructor, yaml.UnsafeLoader) 98 | yaml.add_constructor( 99 | u'!now_naive', now_naive_constructor, yaml.UnsafeLoader) 100 | yaml.add_constructor( 101 | u'!epoch_now', epoch_now_constructor, yaml.UnsafeLoader) 102 | yaml.add_constructor( 103 | u'!epoch_now_in_ms', epoch_now_in_ms_constructor, yaml.UnsafeLoader) 104 | yaml.add_constructor( 105 | u'!rel', relationship_constructor, yaml.UnsafeLoader) 106 | 107 | 108 | def configure_output(use_unicode=False): 109 | """Configure output options of the values loaded by pyyaml. 110 | 111 | :param bool use_unicode: Use unicode constructor for loading strings 112 | """ 113 | if use_unicode: 114 | yaml.add_constructor( 115 | u'tag:yaml.org,2002:str', 116 | Constructor.construct_python_unicode, 117 | ) 118 | 119 | 120 | def load_file(filename, use_unicode=False): 121 | """Load fixtures definition from file. 122 | 123 | :param str filename: 124 | """ 125 | with open(filename) as f: 126 | content = f.read() 127 | 128 | if filename.endswith(".yaml"): 129 | # Load the custom YAML tags 130 | configure_yaml() 131 | configure_output(use_unicode=use_unicode) 132 | content = yaml.unsafe_load(content) 133 | else: 134 | raise ValueError("Unsupported filetype: '%s'" % filename) 135 | 136 | return content 137 | -------------------------------------------------------------------------------- /charlatan/fixture_collection.py: -------------------------------------------------------------------------------- 1 | from charlatan import _compat, utils 2 | from charlatan.fixture import Inheritable 3 | 4 | 5 | def _sorted_iteritems(dct): 6 | """Iterate over the items in the dict in a deterministic fashion.""" 7 | for k, v in sorted(_compat.iteritems(dct)): 8 | yield k, v 9 | 10 | 11 | class FixtureCollection(Inheritable): 12 | 13 | """A FixtureCollection holds Fixture objects.""" 14 | 15 | def __init__(self, key, fixture_manager, 16 | model=None, 17 | models_package=None, 18 | fields=None, 19 | post_creation=None, 20 | inherit_from=None, 21 | depend_on=None, 22 | fixtures=None): 23 | super(FixtureCollection, self).__init__() 24 | self.key = key 25 | self.fixture_manager = fixture_manager 26 | self.fixtures = fixtures or self.container() 27 | 28 | self.key = key 29 | 30 | self.inherit_from = inherit_from 31 | self._has_updated_from_parent = False 32 | 33 | # Stuff that can be inherited. 34 | self.fields = fields or {} 35 | self.model_name = model 36 | self.models_package = models_package 37 | self.post_creation = post_creation or {} 38 | self.depend_on = depend_on 39 | 40 | def __repr__(self): 41 | return "<%s '%s'>" % (self.__class__.__name__, self.key) 42 | 43 | def __iter__(self): 44 | return self.iterator(self.fixtures) 45 | 46 | def get_instance(self, path=None, overrides=None, builder=None): 47 | """Get an instance. 48 | 49 | :param str path: 50 | :param dict overrides: 51 | :param func builder: 52 | """ 53 | if not path: 54 | return self.get_all_instances(overrides=overrides, builder=builder) 55 | 56 | remaining_path = '' 57 | if isinstance(path, _compat.string_types): 58 | path = path.split(".") 59 | first_level = path[0] 60 | remaining_path = ".".join(path[1:]) 61 | 62 | # First try to get the fixture from the cache 63 | instance = self.fixture_manager.cache.get(first_level) 64 | if (not overrides 65 | and instance 66 | and not isinstance(instance, FixtureCollection)): 67 | if not remaining_path: 68 | return instance 69 | return utils.richgetter(instance, remaining_path) 70 | 71 | # Or just get it 72 | fixture = self.get(first_level) 73 | # Then we ask it to return an instance. 74 | return fixture.get_instance(path=remaining_path, 75 | overrides=overrides, 76 | builder=builder, 77 | ) 78 | 79 | def get_all_instances(self, overrides=None, builder=None): 80 | """Get all instances. 81 | 82 | :param dict overrides: 83 | :param func builder: 84 | 85 | .. deprecated:: 0.4.0 86 | Removed format argument. 87 | """ 88 | returned = [] 89 | for name, fixture in self: 90 | instance = fixture.get_instance(overrides=overrides, 91 | builder=builder) 92 | returned.append((name, instance)) 93 | 94 | if self.container is dict: 95 | return dict(returned) 96 | elif self.container is list: 97 | return list(map(lambda f: f[1], returned)) 98 | else: 99 | raise ValueError('Unknown container') 100 | 101 | def extract_relationships(self): 102 | # Just proxy to fixtures in this collection. 103 | for _, fixture in self: 104 | for r in fixture.extract_relationships(): 105 | yield r 106 | 107 | 108 | class DictFixtureCollection(FixtureCollection): 109 | iterator = staticmethod(_sorted_iteritems) 110 | container = dict 111 | 112 | def add(self, name, fixture): 113 | self.fixtures[str(name)] = fixture 114 | 115 | def get(self, path): 116 | """Return a single fixture. 117 | 118 | :param str path: 119 | """ 120 | if path not in self.fixtures: 121 | raise KeyError("No such fixtures: '%s'" % path) 122 | 123 | return self.fixtures[path] 124 | 125 | 126 | class ListFixtureCollection(FixtureCollection): 127 | iterator = enumerate 128 | container = list 129 | 130 | def add(self, _, fixture): 131 | self.fixtures.append(fixture) 132 | 133 | def get(self, path): 134 | """Return a single fixture. 135 | 136 | :param str path: 137 | """ 138 | return self.fixtures[int(path)] 139 | -------------------------------------------------------------------------------- /charlatan/tests/test_file_format.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from sys import version_info 3 | import datetime 4 | 5 | from freezegun import freeze_time 6 | from yaml import add_constructor 7 | from yaml.constructor import SafeConstructor 8 | import pytest 9 | import pytz 10 | import unittest 11 | 12 | from charlatan import testing 13 | from charlatan import file_format 14 | from charlatan.utils import datetime_to_epoch_in_ms 15 | from charlatan.utils import datetime_to_epoch_timestamp 16 | 17 | 18 | def test_non_yaml_file(): 19 | """Verify that we can't open a non-YAML file.""" 20 | with pytest.raises(ValueError): 21 | file_format.load_file("./charlatan/tests/data/test.json") 22 | 23 | 24 | @freeze_time("2014-12-31 11:00:00") 25 | class TestFileFormat(testing.TestCase): 26 | 27 | def setUp(self): 28 | self.current_time = datetime.datetime.utcnow().replace( 29 | tzinfo=pytz.utc) 30 | self.yaml = file_format.load_file( 31 | './charlatan/tests/data/special_tags.yaml' 32 | ) 33 | 34 | def test_now_tag(self): 35 | """Assert !now creates a current datetime.""" 36 | self.assertEqual(self.current_time, self.yaml['current_time']()) 37 | 38 | def test_now_naive_tag(self): 39 | """Assert !now_naive creates a current naive datetime.""" 40 | assert datetime.datetime.utcnow() == self.yaml['current_naive_time']() 41 | 42 | def test_time_offsets(self): 43 | """Assert that !now +1d gives a day in the future.""" 44 | tomorrow = self.current_time + datetime.timedelta(days=1) 45 | self.assertEqual(tomorrow, self.yaml['tomorrow']()) 46 | 47 | def test_epoch_now_tag(self): 48 | """Assert !epoch_now gives integer time.""" 49 | current_epoch_time = datetime_to_epoch_timestamp(self.current_time) 50 | self.assertEqual(current_epoch_time, self.yaml['current_epoch_time']()) 51 | 52 | def test_epoch_now_tag_with_offset(self): 53 | """Assert !epoch_now accepts an offset.""" 54 | tomorrow_datetime = self.current_time + datetime.timedelta(days=1) 55 | tomorrow = datetime_to_epoch_timestamp(tomorrow_datetime) 56 | 57 | self.assertEqual(tomorrow, self.yaml['tomorrow_epoch_time']()) 58 | 59 | def test_epoch_now_in_ms_tag(self): 60 | """Assert !epoch_now_in_ms_tag gives integer time.""" 61 | current_epoch_time_in_ms = datetime_to_epoch_in_ms(self.current_time) 62 | self.assertEqual(current_epoch_time_in_ms, 63 | self.yaml['current_epoch_time_in_ms']()) 64 | 65 | def test_epoch_now_in_ms_tag_with_offset(self): 66 | """Assert !epoch_now_in_ms_tag gives integer time.""" 67 | tomorrow_datetime = self.current_time + datetime.timedelta(days=1) 68 | tomorrow = datetime_to_epoch_in_ms(tomorrow_datetime) 69 | self.assertEqual(tomorrow, self.yaml['tomorrow_epoch_time_in_ms']()) 70 | 71 | def test_rel_tag(self): 72 | """Assert !rel tag makes the value a relationship token.""" 73 | self.assertIsInstance( 74 | self.yaml['relationship'], file_format.RelationshipToken) 75 | 76 | 77 | class TestUnicodeLoad(testing.TestCase): 78 | 79 | def setUp(self): 80 | # preserve the original constructor for strings 81 | self.str_constructor = SafeConstructor.yaml_constructors[ 82 | u'tag:yaml.org,2002:str' 83 | ] 84 | self.yaml = file_format.load_file( 85 | './charlatan/tests/data/unicode.yaml', 86 | use_unicode=True, 87 | ) 88 | 89 | def tearDown(self): 90 | # reset the constructor 91 | add_constructor(u'tag:yaml.org,2002:str', self.str_constructor) 92 | 93 | @unittest.skipIf(version_info[0] == 3, 'Unicode is undefined in Python 3') 94 | def test_strings_are_unicode(self): 95 | """Assert all strings are loaded as unicode.""" 96 | for key, val in self.yaml.items(): 97 | self.assertTrue(isinstance(key, unicode)) # noqa 98 | self.assertTrue(isinstance(val, unicode)) # noqa 99 | 100 | 101 | class TestStringLoad(testing.TestCase): 102 | 103 | def setUp(self): 104 | self.yaml = file_format.load_file( 105 | './charlatan/tests/data/strings.yaml', 106 | ) 107 | 108 | @unittest.skipIf(version_info[0] == 3, 'Iteration has changed in Python 3') 109 | def test_strings_are_strings(self): 110 | """Assert all strings are loaded as strings.""" 111 | for key, val in self.yaml.items(): 112 | self.assertTrue(isinstance(key, str)) 113 | self.assertTrue(isinstance(val, str)) 114 | 115 | @unittest.skipIf(version_info[0] == 2, 'Iteration has changed in Python 3') 116 | def test_strings_are_strings_python3(self): 117 | """Assert all strings are loaded as strings.""" 118 | for key, val in list(self.yaml.items()): 119 | self.assertTrue(isinstance(key, str)) 120 | self.assertTrue(isinstance(val, str)) 121 | -------------------------------------------------------------------------------- /charlatan/tests/test_fixtures_manager.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | from datetime import datetime 3 | 4 | import pytest 5 | import pytz 6 | from freezegun import freeze_time 7 | 8 | from charlatan import testing 9 | from charlatan import depgraph 10 | from charlatan import FixturesManager 11 | 12 | 13 | def test_overrides_and_in_cache(): 14 | manager = FixturesManager() 15 | manager.load('./docs/examples/simple_fixtures.yaml') 16 | # Add it to the cache 17 | manager.install_fixture("toaster") 18 | toaster = manager.install_fixture("toaster", overrides={"color": "blue"}) 19 | assert toaster.color == 'blue' 20 | 21 | 22 | class TestFixturesManager(testing.TestCase): 23 | 24 | def test_load_two_files(self): 25 | """Verify we can load two files.""" 26 | manager = FixturesManager() 27 | manager.load( 28 | './charlatan/tests/data/relationships_without_models.yaml') 29 | manager.load( 30 | './charlatan/tests/data/simple.yaml') 31 | assert 'simple_dict' in manager.keys() 32 | 33 | def test_load_empty_file(self): 34 | """Verify we can load a emtpy file.""" 35 | manager = FixturesManager() 36 | manager.load('./charlatan/tests/data/empty.yaml') 37 | self.assertEqual(list(manager.keys()), []) 38 | 39 | def test_install_fixture(self): 40 | """install_fixture should return the fixture.""" 41 | manager = FixturesManager() 42 | manager.load( 43 | './charlatan/tests/data/relationships_without_models.yaml') 44 | 45 | fixture = manager.install_fixture('simple_dict') 46 | 47 | self.assertEqual(fixture, { 48 | 'field1': 'lolin', 49 | 'field2': 2, 50 | }) 51 | 52 | def test_get_all_fixtures(self): 53 | manager = FixturesManager() 54 | manager.load('./charlatan/tests/data/simple.yaml') 55 | assert len(manager.get_all_fixtures()) == 1 56 | 57 | def test_uninstall_all_fixtures(self): 58 | manager = FixturesManager() 59 | manager.load('./charlatan/tests/data/simple.yaml') 60 | assert len(manager.install_all_fixtures()) == 1 61 | manager.uninstall_all_fixtures() 62 | assert len(manager.installed_keys) == 0 63 | 64 | @freeze_time("2014-12-31 11:00:00") 65 | def test_install_fixture_with_now(self): 66 | """Verify that we can install a fixture with !now tag.""" 67 | manager = FixturesManager() 68 | manager.load('./charlatan/tests/data/simple.yaml') 69 | fixture = manager.install_fixture('fixture') 70 | self.assertEqual(fixture, 71 | {'now': datetime(2014, 12, 30, 11, 0, 72 | tzinfo=pytz.utc)}) 73 | 74 | def test_install_fixture_override(self): 75 | """Verify that we can override a fixture field.""" 76 | manager = FixturesManager() 77 | manager.load('./charlatan/tests/data/simple.yaml') 78 | fixture = manager.install_fixture('fixture', overrides={'now': None}) 79 | self.assertEqual(fixture, {'now': None}) 80 | 81 | def test_uninstall_fixture(self): 82 | manager = FixturesManager() 83 | manager.load( 84 | './charlatan/tests/data/relationships_without_models.yaml') 85 | 86 | manager.install_fixture('simple_dict') 87 | manager.uninstall_fixture('simple_dict') 88 | 89 | # verify we are forgiving with list inputs 90 | manager.install_fixtures('simple_dict') 91 | manager.uninstall_fixtures('simple_dict') 92 | 93 | def test_uninstall_non_installed_fixture(self): 94 | manager = FixturesManager() 95 | manager.load( 96 | './charlatan/tests/data/relationships_without_models.yaml') 97 | manager.uninstall_fixture('simple_dict') 98 | 99 | def test_dependency_parsing(self): 100 | fm = FixturesManager() 101 | fm.load( 102 | './charlatan/tests/data/dependencies.yaml' 103 | ) 104 | assert fm.depgraph.has_edge_between('fixture1', 'fixture2') 105 | assert fm.depgraph.has_edge_between('fixture1', 'fixture3') 106 | assert fm.depgraph.has_edge_between('fixture4', 'fixture3') 107 | assert fm.depgraph.has_edge_between('fixture2', 'fixture4') 108 | 109 | def test_notices_cyclic_dependencies(self): 110 | fm = FixturesManager() 111 | self.assertRaises( 112 | depgraph.HasACycle, 113 | fm.load, 114 | './charlatan/tests/data/cyclic_dependencies.yaml' 115 | ) 116 | 117 | def test_constructs_ancestors(self): 118 | fm = FixturesManager() 119 | fm.load( 120 | './charlatan/tests/data/dependencies.yaml' 121 | ) 122 | assert not fm.cache 123 | # loading fixture3 should load fixture1 and fixture2 also 124 | fm.get_fixture('fixture3') 125 | self.assertIn('fixture1', fm.cache) 126 | self.assertIn('fixture4', fm.cache) 127 | 128 | def test_invalid_hook(self): 129 | """Verify that can't set an invalid hook.""" 130 | manager = FixturesManager() 131 | with pytest.raises(KeyError): 132 | manager.set_hook("invalid", lambda p: p) 133 | 134 | def test_set_hook(self): 135 | """Verify that we can set a hook.""" 136 | manager = FixturesManager() 137 | manager.set_hook("before_save", lambda p: p) 138 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=_build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 10 | set I18NSPHINXOPTS=%SPHINXOPTS% . 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 31 | echo. text to make text files 32 | echo. man to make manual pages 33 | echo. texinfo to make Texinfo files 34 | echo. gettext to make PO message catalogs 35 | echo. changes to make an overview over all changed/added/deprecated items 36 | echo. linkcheck to check all external links for integrity 37 | echo. doctest to run all doctests embedded in the documentation if enabled 38 | goto end 39 | ) 40 | 41 | if "%1" == "clean" ( 42 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 43 | del /q /s %BUILDDIR%\* 44 | goto end 45 | ) 46 | 47 | if "%1" == "html" ( 48 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 49 | if errorlevel 1 exit /b 1 50 | echo. 51 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 52 | goto end 53 | ) 54 | 55 | if "%1" == "dirhtml" ( 56 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 57 | if errorlevel 1 exit /b 1 58 | echo. 59 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 60 | goto end 61 | ) 62 | 63 | if "%1" == "singlehtml" ( 64 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 65 | if errorlevel 1 exit /b 1 66 | echo. 67 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 68 | goto end 69 | ) 70 | 71 | if "%1" == "pickle" ( 72 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 73 | if errorlevel 1 exit /b 1 74 | echo. 75 | echo.Build finished; now you can process the pickle files. 76 | goto end 77 | ) 78 | 79 | if "%1" == "json" ( 80 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 81 | if errorlevel 1 exit /b 1 82 | echo. 83 | echo.Build finished; now you can process the JSON files. 84 | goto end 85 | ) 86 | 87 | if "%1" == "htmlhelp" ( 88 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 89 | if errorlevel 1 exit /b 1 90 | echo. 91 | echo.Build finished; now you can run HTML Help Workshop with the ^ 92 | .hhp project file in %BUILDDIR%/htmlhelp. 93 | goto end 94 | ) 95 | 96 | if "%1" == "qthelp" ( 97 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 98 | if errorlevel 1 exit /b 1 99 | echo. 100 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 101 | .qhcp project file in %BUILDDIR%/qthelp, like this: 102 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\charlatan.qhcp 103 | echo.To view the help file: 104 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\charlatan.ghc 105 | goto end 106 | ) 107 | 108 | if "%1" == "devhelp" ( 109 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 110 | if errorlevel 1 exit /b 1 111 | echo. 112 | echo.Build finished. 113 | goto end 114 | ) 115 | 116 | if "%1" == "epub" ( 117 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 118 | if errorlevel 1 exit /b 1 119 | echo. 120 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 121 | goto end 122 | ) 123 | 124 | if "%1" == "latex" ( 125 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 126 | if errorlevel 1 exit /b 1 127 | echo. 128 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 129 | goto end 130 | ) 131 | 132 | if "%1" == "text" ( 133 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 134 | if errorlevel 1 exit /b 1 135 | echo. 136 | echo.Build finished. The text files are in %BUILDDIR%/text. 137 | goto end 138 | ) 139 | 140 | if "%1" == "man" ( 141 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 142 | if errorlevel 1 exit /b 1 143 | echo. 144 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 145 | goto end 146 | ) 147 | 148 | if "%1" == "texinfo" ( 149 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 150 | if errorlevel 1 exit /b 1 151 | echo. 152 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 153 | goto end 154 | ) 155 | 156 | if "%1" == "gettext" ( 157 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 158 | if errorlevel 1 exit /b 1 159 | echo. 160 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 161 | goto end 162 | ) 163 | 164 | if "%1" == "changes" ( 165 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 166 | if errorlevel 1 exit /b 1 167 | echo. 168 | echo.The overview file is in %BUILDDIR%/changes. 169 | goto end 170 | ) 171 | 172 | if "%1" == "linkcheck" ( 173 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 174 | if errorlevel 1 exit /b 1 175 | echo. 176 | echo.Link check complete; look for any errors in the above output ^ 177 | or in %BUILDDIR%/linkcheck/output.txt. 178 | goto end 179 | ) 180 | 181 | if "%1" == "doctest" ( 182 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 183 | if errorlevel 1 exit /b 1 184 | echo. 185 | echo.Testing of doctests in the sources finished, look at the ^ 186 | results in %BUILDDIR%/doctest/output.txt. 187 | goto end 188 | ) 189 | 190 | :end 191 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_paper_size=a4 12 | PAPEROPT_letter = -D latex_paper_size=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 14 | # the i18n builder cannot share the environment and doctrees with the others 15 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 16 | 17 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 18 | 19 | help: 20 | @echo "Please use \`make ' where is one of" 21 | @echo " html to make standalone HTML files" 22 | @echo " dirhtml to make HTML files named index.html in directories" 23 | @echo " singlehtml to make a single large HTML file" 24 | @echo " pickle to make pickle files" 25 | @echo " json to make JSON files" 26 | @echo " htmlhelp to make HTML files and a HTML help project" 27 | @echo " qthelp to make HTML files and a qthelp project" 28 | @echo " devhelp to make HTML files and a Devhelp project" 29 | @echo " epub to make an epub" 30 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 31 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 32 | @echo " text to make text files" 33 | @echo " man to make manual pages" 34 | @echo " texinfo to make Texinfo files" 35 | @echo " info to make Texinfo files and run them through makeinfo" 36 | @echo " gettext to make PO message catalogs" 37 | @echo " changes to make an overview of all changed/added/deprecated items" 38 | @echo " linkcheck to check all external links for integrity" 39 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 40 | 41 | clean: 42 | -rm -rf $(BUILDDIR)/* 43 | 44 | html: 45 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 46 | @echo 47 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 48 | 49 | dirhtml: 50 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 51 | @echo 52 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 53 | 54 | singlehtml: 55 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 56 | @echo 57 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 58 | 59 | pickle: 60 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 61 | @echo 62 | @echo "Build finished; now you can process the pickle files." 63 | 64 | json: 65 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 66 | @echo 67 | @echo "Build finished; now you can process the JSON files." 68 | 69 | htmlhelp: 70 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 71 | @echo 72 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 73 | ".hhp project file in $(BUILDDIR)/htmlhelp." 74 | 75 | qthelp: 76 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 77 | @echo 78 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 79 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 80 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/charlatan.qhcp" 81 | @echo "To view the help file:" 82 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/charlatan.qhc" 83 | 84 | devhelp: 85 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 86 | @echo 87 | @echo "Build finished." 88 | @echo "To view the help file:" 89 | @echo "# mkdir -p $$HOME/.local/share/devhelp/charlatan" 90 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/charlatan" 91 | @echo "# devhelp" 92 | 93 | epub: 94 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 95 | @echo 96 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 97 | 98 | latex: 99 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 100 | @echo 101 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 102 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 103 | "(use \`make latexpdf' here to do that automatically)." 104 | 105 | latexpdf: 106 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 107 | @echo "Running LaTeX files through pdflatex..." 108 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 109 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 110 | 111 | text: 112 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 113 | @echo 114 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 115 | 116 | man: 117 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 118 | @echo 119 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 120 | 121 | texinfo: 122 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 123 | @echo 124 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 125 | @echo "Run \`make' in that directory to run these through makeinfo" \ 126 | "(use \`make info' here to do that automatically)." 127 | 128 | info: 129 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 130 | @echo "Running Texinfo files through makeinfo..." 131 | make -C $(BUILDDIR)/texinfo info 132 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 133 | 134 | gettext: 135 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 136 | @echo 137 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 138 | 139 | changes: 140 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 141 | @echo 142 | @echo "The overview file is in $(BUILDDIR)/changes." 143 | 144 | linkcheck: 145 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 146 | @echo 147 | @echo "Link check complete; look for any errors in the above output " \ 148 | "or in $(BUILDDIR)/linkcheck/output.txt." 149 | 150 | doctest: 151 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 152 | @echo "Testing of doctests in the sources finished, look at the " \ 153 | "results in $(BUILDDIR)/doctest/output.txt." 154 | -------------------------------------------------------------------------------- /docs/quickstart.rst: -------------------------------------------------------------------------------- 1 | Quickstart 2 | ========== 3 | 4 | A simple example 5 | ---------------- 6 | 7 | Let's say we have the following model: 8 | 9 | .. literalinclude:: ../charlatan/tests/fixtures/simple_models.py 10 | :language: python 11 | 12 | Let's define a very simple fixtures YAML file: 13 | 14 | .. literalinclude:: examples/simple_fixtures.yaml 15 | :language: yaml 16 | 17 | In this example: 18 | 19 | * ``toaster`` and ``toasts`` are the fixture keys. 20 | * ``fields`` is provided as argument when instantiating the class: 21 | ``Toaster(**fields)``. 22 | * ``model`` is the path to the model that we defined. 23 | * ``!rel`` lets you create relationships by pointing to another fixture key. 24 | 25 | You first need to load a fixtures file (do it once for the whole test suite) 26 | with :py:meth:`charlatan.FixturesManager.load`: 27 | 28 | .. doctest:: 29 | 30 | >>> import charlatan 31 | >>> fixtures_manager = charlatan.FixturesManager() 32 | >>> fixtures_manager.load("./docs/examples/simple_fixtures.yaml", 33 | ... models_package="toaster.models") 34 | >>> toaster = fixtures_manager.install_fixture("toaster") 35 | >>> toaster.color 36 | 'red' 37 | >>> toaster.slots 38 | 5 39 | >>> toaster.content 40 | ['Toast 1', 'Toast 2'] 41 | 42 | Voila! 43 | 44 | Factory features 45 | ---------------- 46 | 47 | `Charlatan` provides you with factory features. In particular, you can override 48 | a fixture's defined attributes: 49 | 50 | .. doctest:: 51 | 52 | >>> toaster = fixtures_manager.install_fixture("toaster", 53 | ... overrides={"color": "blue"}) 54 | >>> toaster.color 55 | 'blue' 56 | 57 | You can also use inheritance: 58 | 59 | .. doctest:: 60 | 61 | >>> toaster = fixtures_manager.install_fixture("toaster_green") 62 | >>> toaster.color 63 | 'green' 64 | 65 | Using charlatan in test cases 66 | ----------------------------- 67 | 68 | `Charlatan` works best when used with :py:class:`unittest.TestCase`. Your test 69 | class needs to inherit from :py:class:`charlatan.FixturesManagerMixin`. 70 | 71 | `Charlatan` uses an internal cache to store fixtures instance (in particular to 72 | create relationships). If you are resetting your database after each tests 73 | (using transactions or by manually truncating all tables), you need to clean 74 | the cache in :py:meth:`TestCase.setUp`, otherwise `Charlatan` will try 75 | accessing objects that are not anymore in the sqlalchemy session. 76 | 77 | .. literalinclude:: ../charlatan/tests/test_simple_testcase.py 78 | :language: python 79 | 80 | Using fixtures 81 | -------------- 82 | 83 | There are multiple ways to require and use fixtures. When you install a fixture 84 | using the :py:class:`charlatan.FixturesManagerMixin`, it gets attached to the 85 | instance and can be accessed as an instance attribute (e.g. ``self.toaster``). 86 | 87 | For each tests, in setUp and tearDown 88 | """"""""""""""""""""""""""""""""""""" 89 | 90 | .. code-block:: python 91 | 92 | class MyTest(FixturesManagerMixin): 93 | 94 | def setUp(self): 95 | # This will create self.toaster and self.brioche 96 | self.install_fixtures(("toaster", "brioche")) 97 | 98 | def test_toaster(self): 99 | """Verify that a toaster toasts.""" 100 | self.toaster.toast(self.brioche) 101 | 102 | For a single test 103 | """"""""""""""""" 104 | 105 | .. code-block:: python 106 | 107 | class MyTest(FixturesMixin): 108 | 109 | def test_toaster(self): 110 | self.install_fixture("toaster") 111 | 112 | With pytest 113 | """"""""""" 114 | 115 | It's extremely easy to use charlatan with pytest. There are multiple ways to 116 | achieve nice readability, here's one possibility. 117 | 118 | In ``conftest.py``: 119 | 120 | .. code-block:: python 121 | 122 | import pytest 123 | 124 | 125 | @pytest.fixture 126 | def get_fixture(request): 127 | request.addfinalizer(fixtures_manager.clean_cache) 128 | return fixtures_manager.get_fixture 129 | 130 | In your test file: 131 | 132 | .. code-block:: python 133 | 134 | def test_toaster(get_fixture): 135 | """Verify that a toaster toasts.""" 136 | toaster = get_fixture('toaster') 137 | toast = get_fixture('toast') 138 | ... 139 | 140 | Getting a fixture without saving it 141 | """"""""""""""""""""""""""""""""""" 142 | 143 | If you want to have complete control over the fixture, you can also get it 144 | without saving it nor attaching it to the test class: 145 | 146 | 147 | .. code-block:: python 148 | 149 | class MyTest(FixturesManagerMixin): 150 | 151 | def test_toaster(self): 152 | self.toaster = self.get_fixture("toaster") 153 | self.toaster.brand = "Flying" 154 | self.toaster.save() 155 | 156 | What happens when you install a fixture 157 | """"""""""""""""""""""""""""""""""""""" 158 | 159 | Here's the default process (you can modify part or all of it using :ref:`hooks` 160 | or :ref:`builders`): 161 | 162 | 1. The fixture is instantiated: ``Model(**fields)``. 163 | 2. If there's any post creation hook, they are run (see :ref:`post_creation` 164 | for more information). 165 | 3. The fixture is then saved. If it's a sqlalchemy model, charlatan will detect 166 | it, add it to the session and commit it (``db_session.add(instance); db_session.commit()``). 167 | If it's not a sqlalchemy model, charlatan will try to call a `save` method 168 | on the instance. If there's no such method, charlatan will do nothing. 169 | 170 | :ref:`hooks` are also supported. 171 | 172 | Uninstalling fixtures 173 | """"""""""""""""""""" 174 | 175 | Because charlatan is not coupled with the persistence layer, it does not have 176 | strong opinions about resetting the world after a test runs. There's multiple 177 | ways to handle test tear down: 178 | 179 | * Wrap test inside a transaction (if you're using sqlalchemy, its documentation 180 | has a `good 181 | explanation `_ 182 | about how to achieve that). 183 | * Drop and recreate the database (not really efficient). 184 | * Install and uninstall fixtures explicitly (you have to keep track of them 185 | though, if you forget to uninstall one fixture it will leak in the other 186 | tests). See 187 | :py:meth:`charlatan.FixturesManager.uninstall_fixture`. 188 | -------------------------------------------------------------------------------- /charlatan/utils.py: -------------------------------------------------------------------------------- 1 | import calendar 2 | import datetime 3 | import functools 4 | import itertools 5 | import operator 6 | import re 7 | import collections 8 | 9 | from charlatan import _compat 10 | 11 | VALID_SIGNS = frozenset(['-', '+']) 12 | 13 | 14 | def get_timedelta(delta): 15 | """Return timedelta from string. 16 | 17 | :param str delta: 18 | 19 | :rtype: :py:class:`datetime.timedelta` instance 20 | 21 | >>> get_timedelta("") 22 | datetime.timedelta(0) 23 | >>> get_timedelta("+1h") 24 | datetime.timedelta(0, 3600) 25 | >>> get_timedelta("+10h") 26 | datetime.timedelta(0, 36000) 27 | >>> get_timedelta("-10d") 28 | datetime.timedelta(-10) 29 | >>> get_timedelta("+1m") 30 | datetime.timedelta(30) 31 | >>> get_timedelta("-1y") 32 | datetime.timedelta(-365) 33 | >>> get_timedelta("+10d2h") 34 | datetime.timedelta(10, 7200) 35 | >>> get_timedelta("-10d2h") 36 | datetime.timedelta(-11, 79200) 37 | >>> get_timedelta("-21y2m1d24h") 38 | datetime.timedelta(-7727) 39 | >>> get_timedelta("+5M") 40 | datetime.timedelta(0, 300) 41 | 42 | """ 43 | if not delta: 44 | return datetime.timedelta() 45 | sign = delta[0] 46 | assert sign in VALID_SIGNS 47 | delta = delta[1:] 48 | timedelta_kwargs = {} 49 | 50 | # re.split returns empty strings if the capturing groups matches the 51 | # start and the end of string. 52 | for part in filter(lambda p: p, re.split(r"(\d+\w)", delta)): 53 | amount, unit = re.findall(r"(\d+)([ymdhMs])", part)[0] 54 | units = { 55 | "s": "seconds", 56 | "M": "minutes", 57 | "h": "hours", 58 | "d": "days", 59 | "m": "months", 60 | "y": "years" 61 | } 62 | timedelta_kwargs[units[unit]] = int(amount) 63 | 64 | delta = extended_timedelta(**timedelta_kwargs) 65 | if sign == '-': 66 | return -delta 67 | else: 68 | return delta 69 | 70 | 71 | def extended_timedelta(**kwargs): 72 | """Return a :py:class:`timedelta` object based on the arguments. 73 | 74 | :param integer years: 75 | :param integer months: 76 | :param integer days: 77 | :rtype: :py:class:`timedelta` instance 78 | 79 | Since :py:class:`timedelta`'s largest unit are days, :py:class:`timedelta` 80 | objects cannot be created with a number of months or years as an argument. 81 | This function lets you create :py:class:`timedelta` objects based on a 82 | number of days, months and years. 83 | 84 | >>> extended_timedelta(months=1) 85 | datetime.timedelta(30) 86 | >>> extended_timedelta(years=1) 87 | datetime.timedelta(365) 88 | >>> extended_timedelta(days=1, months=1, years=1) 89 | datetime.timedelta(396) 90 | >>> extended_timedelta(hours=1) 91 | datetime.timedelta(0, 3600) 92 | """ 93 | number_of_days = { 94 | "days": 1, 95 | "months": 30, 96 | "years": 365} 97 | 98 | days = [] 99 | kwargs_copy = kwargs.copy() # So that we can remove values from kwargs 100 | for k in kwargs_copy: 101 | if k in number_of_days: 102 | days.append([number_of_days[k], kwargs.pop(k)]) 103 | 104 | if days: 105 | add = int(functools.reduce( 106 | operator.add, itertools.starmap(operator.mul, days))) 107 | kwargs["days"] = kwargs.setdefault("days", 0) + add 108 | 109 | return datetime.timedelta(**kwargs) 110 | 111 | 112 | def datetime_to_epoch_timestamp(a_datetime): 113 | """Return the epoch timestamp for the given datetime. 114 | 115 | :param datetime a_datetime: The datetime to translate 116 | :rtype: float 117 | 118 | >>> a_datetime = datetime.datetime(2013, 11, 21, 1, 33, 11, 160611) 119 | >>> datetime_to_epoch_timestamp(a_datetime) 120 | 1384997591.160611 121 | """ 122 | return (calendar.timegm(a_datetime.utctimetuple()) + a_datetime.microsecond / 1000000.0) # noqa 123 | 124 | 125 | def datetime_to_epoch_in_ms(a_datetime): 126 | """Return the epoch timestamp for the given datetime. 127 | 128 | :param datetime a_datetime: The datetime to translate 129 | :rtype: int 130 | 131 | >>> a_datetime = datetime.datetime(2013, 11, 21, 1, 33, 11, 160611) 132 | >>> datetime_to_epoch_timestamp(a_datetime) 133 | 1384997591.160611 134 | >>> datetime_to_epoch_in_ms(a_datetime) 135 | 1384997591161 136 | """ 137 | seconds_in_float = datetime_to_epoch_timestamp(a_datetime) 138 | return int(round(seconds_in_float * 1000, 0)) 139 | 140 | 141 | # TODO: does not copy the function signature 142 | # see http://stackoverflow.com/questions/2982974/copy-call-signature-to-decorator # noqa 143 | 144 | 145 | def copy_docstring_from(klass): 146 | """Copy docstring from another class, using the same function name.""" 147 | def wrapper(func): 148 | func.__doc__ = getattr(klass, func.__name__).__doc__ 149 | 150 | @functools.wraps(func) 151 | def wrapped(*args, **kwargs): 152 | return func(*args, **kwargs) 153 | 154 | return wrapped 155 | 156 | return wrapper 157 | 158 | 159 | def safe_iteritems(items): 160 | """Safely iterate over a dict or a list.""" 161 | # For dictionaries, iterate over key, value and for lists iterate over 162 | # index, item 163 | if hasattr(items, 'items'): 164 | return _compat.iteritems(items) 165 | else: 166 | return enumerate(items) 167 | 168 | 169 | def is_sqlalchemy_model(instance): 170 | """Return True if instance is an SQLAlchemy model instance.""" 171 | from sqlalchemy.orm.util import class_mapper 172 | from sqlalchemy.orm.exc import UnmappedClassError 173 | 174 | try: 175 | class_mapper(instance.__class__) 176 | 177 | except UnmappedClassError: 178 | return False 179 | 180 | else: 181 | return True 182 | 183 | 184 | def richgetter(obj, path): 185 | """Return a attrgetter + item getter.""" 186 | for name in path.split("."): 187 | if isinstance(obj, collections.Mapping): 188 | obj = obj[name] 189 | elif isinstance(obj, collections.Sequence): 190 | obj = obj[int(name)] # force int type for list indexes 191 | else: 192 | obj = getattr(obj, name) 193 | 194 | return obj 195 | 196 | 197 | def deep_update(source, overrides): 198 | """Update a nested dictionary or similar mapping. 199 | 200 | Modify ``source`` in place. 201 | """ 202 | # http://stackoverflow.com/questions/3232943/update-value-of-a-nested-dictionary-of-varying-depth # noqa 203 | for key, value in _compat.iteritems(overrides): 204 | if isinstance(value, collections.Mapping) and value: 205 | returned = deep_update(source.get(key, {}), value) 206 | source[key] = returned 207 | else: 208 | source[key] = overrides[key] 209 | return source 210 | -------------------------------------------------------------------------------- /CHANGES.rst: -------------------------------------------------------------------------------- 1 | Changelog for Charlatan 2 | ======================= 3 | 4 | 0.4.8 (unreleased) 5 | ------------------ 6 | 7 | - Nothing changed yet. 8 | 9 | 10 | 0.4.7 (2019-08-30) 11 | ------------------ 12 | 13 | - Silence PyYAML safety warnings 14 | 15 | 16 | 0.4.6 (2015-09-22) 17 | ------------------ 18 | 19 | - Add support for ``epoch_now_in_ms`` (thanks to @chunyan) 20 | 21 | 0.4.5 (2015-05-29) 22 | ------------------ 23 | 24 | - Add ``deep_inherit`` to allow nested inheritance of fields. 25 | 26 | 0.4.4 (2015-05-28) 27 | ------------------ 28 | 29 | - Added ``!now_naive`` YAML file command to return naive datetime. 30 | 31 | 0.4.3 (2015-05-26) 32 | ------------------ 33 | 34 | - Fixed anonymous list objects name resolution (thanks to @jvrsantacruz) 35 | 36 | 0.4.2 (2015-05-19) 37 | ------------------ 38 | 39 | - **Breaking change**: the ``!now`` YAML command now returns timezone-aware 40 | datetime by default. You can change that behavior by changing 41 | `charlatan.file_format.TIMEZONE_AWARE`. 42 | - Fixed bug where uninstalling a sqlalchemy fixture would not commit the delete 43 | to the session. 44 | - Fixed bug where dict fixtures could not reference fields from other collections of dicts. 45 | 46 | 0.4.1 (2015-02-26) 47 | ------------------ 48 | 49 | - Fixed bug where ``!rel a_database_model.id``, where ``id`` is a primary key 50 | generated by the database, would be ``None`` because of how fixtures are 51 | cached. 52 | - Removed ``as_list`` and ``as_dict`` feature. It was unnecessarily complex and 53 | would not play well with caching fixtures. 54 | 55 | 0.4.0 (2015-02-18) 56 | ------------------ 57 | 58 | - **Breaking change**: ``get_builder`` and ``delete_builder`` arguments were 59 | added to :py:class:`charlatan.FixturesManager`. 60 | - **Breaking change**: ``delete_instance``, ``save_instance`` methods were 61 | deleted in favor of using builders (see below). 62 | - **Breaking change**: ``fields`` argument on 63 | :py:class:`charlatan.fixture.Fixture` and fixtures collection class has 64 | been renamed ``overrides`` for consistency reasons. 65 | - **Breaking change**: ``attrs`` argument on 66 | :py:class:`charlatan.FixturesManager` been renamed ``overrides`` for 67 | consistency reasons. 68 | - **Breaking change**: deleting fixtures will not return anything. It used to 69 | return the fixture or list of fixtures that were successfully deleted. It has 70 | been removed to apply the command query separation pattern. There are other 71 | ways to check which fixtures are installed, and hooks or builders can be used 72 | to customize deletion. 73 | - **Breaking change**: ``do_not_save`` and ``do_not_delete`` arguments have 74 | been removed from all functions, in favor of using builders. 75 | - The notion of :py:class:`charlatan.builder.Builder` was added. This allows 76 | customizing how fixtures are instantiated and installed. A ``builder`` 77 | argument has been added to most method dealing with getting, installing or 78 | deleting fixtures. Sane defaults have been added in most places. 79 | - Improve documentation about using pytest with charlatan. 80 | - Fix bug preventing being able to load multiple fixtures file. 81 | 82 | 0.3.12 (2015-01-14) 83 | ------------------- 84 | 85 | - Do not install the class' ``fixtures`` variable on 86 | :py:class:`charlatan.FixturesManagerMixin` initialization. This can lead to 87 | bad pattern where a huge list of fixtures is installed for each test, even 88 | though each test uses only a few. Also, it's safer to be explicit about this 89 | behavior and let the user have this automatic installation. Note that you can 90 | easily reimplement this behavior by subclassing or installing those in the 91 | class ``setUp`` method. 92 | 93 | 0.3.11 (2015-01-06) 94 | ------------------- 95 | 96 | - Fix getting relationships with fields that are nested more than one level 97 | 98 | 0.3.10 (2014-12-31) 99 | ------------------- 100 | 101 | - Get ``utcnow`` at fixture instantiation time, to allow using ``freezegun`` 102 | intuitively 103 | 104 | 0.3.9 (2014-11-13) 105 | ------------------ 106 | 107 | - Fix saving collection of fixtures to database (thanks to @joegilley) 108 | 109 | 0.3.8 (2014-08-19) 110 | ------------------ 111 | 112 | - Support loading of globbed filenames 113 | 114 | 0.3.7 (2014-07-07) 115 | ------------------ 116 | 117 | - Support loading of multiple fixtures files 118 | - Remove include_relationships option in instance creation 119 | 120 | 0.3.6 (2014-06-02) 121 | ------------------ 122 | 123 | - Update PYYaml 124 | 125 | 0.3.5 (2014-06-02) 126 | ------------------ 127 | 128 | - Support loading all strings as unicode 129 | 130 | 0.3.4 (2014-01-21) 131 | ------------------ 132 | 133 | - Fix getting attribute from relationships 134 | 135 | 0.3.3 (2014-01-18) 136 | ------------------ 137 | 138 | - Add support for Python 3 139 | 140 | 0.3.2 (2014-01-16) 141 | ------------------ 142 | 143 | - Add ability to uninstall fixtures (thanks to @JordanB) 144 | 145 | 0.3.1 (2014-01-10) 146 | ------------------ 147 | 148 | - Numerous tests added, a lot of cleanup. 149 | - Clarification in documentation. 150 | - Remove ``load``, ``set_hook`` and ``install_all_fixtures`` shortcuts from 151 | charlatan package. 152 | - Remove ``FIXTURES_MANAGER`` singleton. Remove ``charlatan.fixtures_manager`` 153 | shortcut. 154 | - Remove ``db_session`` argument to ``FixturesManager.load``. 155 | - Add ``db_session`` argument to ``FixturesManager`` constructor. 156 | - Remove ``charlatan.fixtures_manager.FixturesMixin``. Replaced by 157 | ``charlatan.testcase.FixturesManagerMixin``. 158 | - ``FixturesManagerMixin`` now exposes pretty much the same method as 159 | ``FixturesManager``. 160 | - ``FixturesManagerMixin``'s ``use_fixtures_manager`` was renamed 161 | ``init_fixtures``. 162 | 163 | 0.2.9 (2013-11-20) 164 | ------------------ 165 | 166 | - Add ``!epoch_now`` for Unix timestamps (thanks to @erikformella) 167 | 168 | 0.2.8 (2013-11-12) 169 | ------------------ 170 | 171 | - Add ability to point to a list fixture (thanks to @erikformella) 172 | 173 | 0.2.7 (2013-10-24) 174 | ------------------ 175 | 176 | - Add ability to define dependencies outside of fields through the `depend_on` 177 | key in the yaml file (thanks to @Roguelazer) 178 | 179 | 0.2.6 (2013-09-06) 180 | ------------------ 181 | 182 | - Fix regression that broke API. install_fixture started returning the fixture 183 | as well as its name. (thanks to @erikformella) 184 | 185 | 0.2.5 (2013-09-06) 186 | ------------------ 187 | 188 | - Allow relationships to be used in dicts and lists. (thanks to @erikformella) 189 | - Allow for seconds and minutes in relative timestamps (thanks to @kmnovak) 190 | 191 | 0.2.4 (2013-08-08) 192 | ------------------ 193 | 194 | - Empty models are allowed so that dict ands lists can be used as fixtures. 195 | - Fixtures can now inherits from other fixtures. 196 | 197 | 0.2.3 (2013-06-28) 198 | ------------------ 199 | 200 | - Added ability to link to a relationship's attribute in YAML file. 201 | - Added ability to use ``!rel`` in ``post_creation``. 202 | 203 | 0.1.2 (2013-04-01) 204 | ------------------ 205 | 206 | - Started tracking changes 207 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa 2 | # -*- coding: utf-8 -*- 3 | # 4 | # charlatan documentation build configuration file, created by 5 | # sphinx-quickstart on Wed Feb 6 11:21:22 2013. 6 | # 7 | # This file is execfile()d with the current directory set to its containing dir. 8 | # 9 | # Note that not all possible configuration values are present in this 10 | # autogenerated file. 11 | # 12 | # All configuration values have a default; values that are commented out 13 | # serve to show the default. 14 | 15 | from __future__ import print_function 16 | import sys, os 17 | 18 | # If extensions (or modules to document with autodoc) are in another directory, 19 | # add these directories to sys.path here. If the directory is relative to the 20 | # documentation root, use os.path.abspath to make it absolute, like shown here. 21 | sys.path.insert(0, os.path.abspath('..')) 22 | 23 | # -- General configuration ----------------------------------------------------- 24 | 25 | # If your documentation needs a minimal Sphinx version, state it here. 26 | #needs_sphinx = '1.0' 27 | 28 | # Add any Sphinx extension module names here, as strings. They can be extensions 29 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 30 | extensions = ['sphinx.ext.autodoc', 'sphinx.ext.doctest'] 31 | 32 | # Add any paths that contain templates here, relative to this directory. 33 | templates_path = ['_templates'] 34 | 35 | # The suffix of source filenames. 36 | source_suffix = '.rst' 37 | 38 | # The encoding of source files. 39 | #source_encoding = 'utf-8-sig' 40 | 41 | # The master toctree document. 42 | master_doc = 'index' 43 | 44 | # General information about the project. 45 | project = u'charlatan' 46 | copyright = u'2013, Charles-Axel Dein (Uber)' 47 | 48 | # The version info for the project you're documenting, acts as replacement for 49 | # |version| and |release|, also used in various other places throughout the 50 | # built documents. 51 | # 52 | 53 | import pkg_resources 54 | try: 55 | release = pkg_resources.get_distribution('charlatan').version 56 | except pkg_resources.DistributionNotFound: 57 | print("Distribution information not found. Run 'setup.py develop'") 58 | sys.exit(1) 59 | del pkg_resources 60 | version = '.'.join(release.split('.')[:2]) 61 | 62 | # The language for content autogenerated by Sphinx. Refer to documentation 63 | # for a list of supported languages. 64 | #language = None 65 | 66 | # There are two options for replacing |today|: either, you set today to some 67 | # non-false value, then it is used: 68 | #today = '' 69 | # Else, today_fmt is used as the format for a strftime call. 70 | #today_fmt = '%B %d, %Y' 71 | 72 | # List of patterns, relative to source directory, that match files and 73 | # directories to ignore when looking for source files. 74 | exclude_patterns = ['_build'] 75 | 76 | # The reST default role (used for this markup: `text`) to use for all documents. 77 | #default_role = None 78 | 79 | # If true, '()' will be appended to :func: etc. cross-reference text. 80 | #add_function_parentheses = True 81 | 82 | # If true, the current module name will be prepended to all description 83 | # unit titles (such as .. function::). 84 | #add_module_names = True 85 | 86 | # If true, sectionauthor and moduleauthor directives will be shown in the 87 | # output. They are ignored by default. 88 | #show_authors = False 89 | 90 | # The name of the Pygments (syntax highlighting) style to use. 91 | pygments_style = 'sphinx' 92 | 93 | # A list of ignored prefixes for module index sorting. 94 | #modindex_common_prefix = [] 95 | 96 | 97 | # -- Options for HTML output --------------------------------------------------- 98 | 99 | # The theme to use for HTML and HTML Help pages. See the documentation for 100 | # a list of builtin themes. 101 | html_theme = 'default' 102 | 103 | # Theme options are theme-specific and customize the look and feel of a theme 104 | # further. For a list of options available for each theme, see the 105 | # documentation. 106 | #html_theme_options = {} 107 | 108 | # Add any paths that contain custom themes here, relative to this directory. 109 | #html_theme_path = [] 110 | 111 | # The name for this set of Sphinx documents. If None, it defaults to 112 | # " v documentation". 113 | #html_title = None 114 | 115 | # A shorter title for the navigation bar. Default is the same as html_title. 116 | #html_short_title = None 117 | 118 | # The name of an image file (relative to this directory) to place at the top 119 | # of the sidebar. 120 | #html_logo = None 121 | 122 | # The name of an image file (within the static path) to use as favicon of the 123 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 124 | # pixels large. 125 | #html_favicon = None 126 | 127 | # Add any paths that contain custom static files (such as style sheets) here, 128 | # relative to this directory. They are copied after the builtin static files, 129 | # so a file named "default.css" will overwrite the builtin "default.css". 130 | html_static_path = ['_static'] 131 | 132 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 133 | # using the given strftime format. 134 | #html_last_updated_fmt = '%b %d, %Y' 135 | 136 | # If true, SmartyPants will be used to convert quotes and dashes to 137 | # typographically correct entities. 138 | #html_use_smartypants = True 139 | 140 | # Custom sidebar templates, maps document names to template names. 141 | #html_sidebars = {} 142 | 143 | # Additional templates that should be rendered to pages, maps page names to 144 | # template names. 145 | #html_additional_pages = {} 146 | 147 | # If false, no module index is generated. 148 | #html_domain_indices = True 149 | 150 | # If false, no index is generated. 151 | #html_use_index = True 152 | 153 | # If true, the index is split into individual pages for each letter. 154 | #html_split_index = False 155 | 156 | # If true, links to the reST sources are added to the pages. 157 | #html_show_sourcelink = True 158 | 159 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 160 | #html_show_sphinx = True 161 | 162 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 163 | #html_show_copyright = True 164 | 165 | # If true, an OpenSearch description file will be output, and all pages will 166 | # contain a tag referring to it. The value of this option must be the 167 | # base URL from which the finished HTML is served. 168 | #html_use_opensearch = '' 169 | 170 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 171 | #html_file_suffix = None 172 | 173 | # Output file base name for HTML help builder. 174 | htmlhelp_basename = 'charlatandoc' 175 | 176 | 177 | # -- Options for LaTeX output -------------------------------------------------- 178 | 179 | latex_elements = { 180 | # The paper size ('letterpaper' or 'a4paper'). 181 | #'papersize': 'letterpaper', 182 | 183 | # The font size ('10pt', '11pt' or '12pt'). 184 | #'pointsize': '10pt', 185 | 186 | # Additional stuff for the LaTeX preamble. 187 | #'preamble': '', 188 | } 189 | 190 | # Grouping the document tree into LaTeX files. List of tuples 191 | # (source start file, target name, title, author, documentclass [howto/manual]). 192 | latex_documents = [ 193 | ('index', 'charlatan.tex', u'charlatan Documentation', 194 | u'Charles-Axel Dein', 'manual'), 195 | ] 196 | 197 | # The name of an image file (relative to this directory) to place at the top of 198 | # the title page. 199 | #latex_logo = None 200 | 201 | # For "manual" documents, if this is true, then toplevel headings are parts, 202 | # not chapters. 203 | #latex_use_parts = False 204 | 205 | # If true, show page references after internal links. 206 | #latex_show_pagerefs = False 207 | 208 | # If true, show URL addresses after external links. 209 | #latex_show_urls = False 210 | 211 | # Documents to append as an appendix to all manuals. 212 | #latex_appendices = [] 213 | 214 | # If false, no module index is generated. 215 | #latex_domain_indices = True 216 | 217 | 218 | # -- Options for manual page output -------------------------------------------- 219 | 220 | # One entry per manual page. List of tuples 221 | # (source start file, name, description, authors, manual section). 222 | man_pages = [ 223 | ('index', 'charlatan', u'charlatan Documentation', 224 | [u'Charles-Axel Dein'], 1) 225 | ] 226 | 227 | # If true, show URL addresses after external links. 228 | #man_show_urls = False 229 | 230 | 231 | # -- Options for Texinfo output ------------------------------------------------ 232 | 233 | # Grouping the document tree into Texinfo files. List of tuples 234 | # (source start file, target name, title, author, 235 | # dir menu entry, description, category) 236 | texinfo_documents = [ 237 | ('index', 'charlatan', u'charlatan Documentation', 238 | u'Charles-Axel Dein', 'charlatan', 'One line description of project.', 239 | 'Miscellaneous'), 240 | ] 241 | 242 | # Documents to append as an appendix to all manuals. 243 | #texinfo_appendices = [] 244 | 245 | # If false, no module index is generated. 246 | #texinfo_domain_indices = True 247 | 248 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 249 | #texinfo_show_urls = 'footnote' 250 | 251 | 252 | # -- Options for Epub output --------------------------------------------------- 253 | 254 | # Bibliographic Dublin Core info. 255 | epub_title = u'charlatan' 256 | epub_author = u'Charles-Axel Dein' 257 | epub_publisher = u'Charles-Axel Dein' 258 | epub_copyright = u'2013, Charles-Axel Dein' 259 | 260 | # The language of the text. It defaults to the language option 261 | # or en if the language is not set. 262 | #epub_language = '' 263 | 264 | # The scheme of the identifier. Typical schemes are ISBN or URL. 265 | #epub_scheme = '' 266 | 267 | # The unique identifier of the text. This can be a ISBN number 268 | # or the project homepage. 269 | #epub_identifier = '' 270 | 271 | # A unique identification for the text. 272 | #epub_uid = '' 273 | 274 | # A tuple containing the cover image and cover page html template filenames. 275 | #epub_cover = () 276 | 277 | # HTML files that should be inserted before the pages created by sphinx. 278 | # The format is a list of tuples containing the path and title. 279 | #epub_pre_files = [] 280 | 281 | # HTML files shat should be inserted after the pages created by sphinx. 282 | # The format is a list of tuples containing the path and title. 283 | #epub_post_files = [] 284 | 285 | # A list of files that should not be packed into the epub file. 286 | #epub_exclude_files = [] 287 | 288 | # The depth of the table of contents in toc.ncx. 289 | #epub_tocdepth = 3 290 | 291 | # Allow duplicate toc entries. 292 | #epub_tocdup = True 293 | -------------------------------------------------------------------------------- /docs/file-format.rst: -------------------------------------------------------------------------------- 1 | File format 2 | =========== 3 | 4 | .. testsetup:: * 5 | 6 | from charlatan import FixturesManager 7 | 8 | charlatan only supports YAML at time of writing. 9 | 10 | Fixtures are defined in a YAML file. Here is its general structure: 11 | 12 | .. literalinclude:: examples/fixtures.yaml 13 | :language: yaml 14 | 15 | In this example: 16 | 17 | * ``toaster``, ``toast1`` and ``toast2`` are the fixture keys. 18 | * ``model`` is where to get the model. Both relative and absolute addressing are supported 19 | * ``fields`` are provided as argument when instantiating the class: 20 | ``Toaster(**fields)``. 21 | * ``!rel`` lets you create relationships by pointing to another fixture key. 22 | * ``!now`` lets you enter timestamps. It supports basic operations 23 | (adding/subtracting days, months, years). It is evaluated when the fixture 24 | is instantiated. 25 | * ``!epoch_now`` generates epoch timestamps and supports the same operations as 26 | ``!now``. 27 | 28 | .. NOTE:: 29 | Inside ``fields``, ``!now`` is supported only as a first level list item, 30 | or as a dictionary value. 31 | 32 | 33 | Defining a fixture 34 | ------------------ 35 | 36 | A fixture has an identifier (in the example above, ``toaster`` is one of 37 | the fixture identifiers), as well as the following configuration: 38 | 39 | * ``fields``: a dictionary for which keys are attribute, and values are their 40 | values 41 | * ``model`` gives information about how to retrieve the model 42 | * ``post_creation`` lets you have some attribute values be assigned after 43 | instantiation. 44 | 45 | Inheritance 46 | ----------- 47 | 48 | Fixtures can inherit from other fixtures. 49 | 50 | .. literalinclude:: examples/fixtures_inheritance.yaml 51 | :language: yaml 52 | 53 | .. doctest:: 54 | 55 | >>> import pprint 56 | >>> from charlatan import FixturesManager 57 | >>> manager = FixturesManager() 58 | >>> manager.load("docs/examples/fixtures_inheritance.yaml") 59 | >>> manager.get_fixture("first") 60 | {'foo': 'bar'} 61 | >>> manager.get_fixture("second") 62 | {'foo': 'bar'} 63 | >>> pprint.pprint(manager.get_fixture("third")) 64 | {'foo': 'bar', 'toaster': 'toasted'} 65 | >>> fourth = manager.get_fixture("fourth") 66 | >>> fourth 67 | Counter({'foo': 'bar'}) 68 | >>> fourth.__class__.__name__ 69 | 'Counter' 70 | >>> fifth = manager.get_fixture("fifth") 71 | >>> fifth 72 | Counter({'toaster': 'toasted', 'foo': 'bar'}) 73 | >>> fifth.__class__.__name__ 74 | 'Counter' 75 | 76 | If your fields are dict, then the first-level key will override everything, 77 | unless you use ``deep_inherit``: 78 | 79 | .. literalinclude:: ../charlatan/tests/example/data/deep_inherit.yaml 80 | :language: yaml 81 | 82 | Example test: 83 | 84 | .. literalinclude:: ../charlatan/tests/example/test_deep_inherit.py 85 | 86 | .. versionadded:: 0.4.5 87 | You can use ``deep_inherit`` to trigger nested inheritance for dicts. 88 | 89 | .. versionadded:: 0.2.4 90 | Fixtures can now inherits from other fixtures. 91 | 92 | Having dictionaries as fixtures 93 | ------------------------------- 94 | 95 | If you don't specify the model, the content of ``fields`` will be returned as 96 | is. This is useful if you want to enter a dictionary or a list directly. 97 | 98 | .. literalinclude:: examples/fixtures_dict.yaml 99 | :language: yaml 100 | 101 | .. doctest:: 102 | 103 | >>> manager = FixturesManager() 104 | >>> manager.load("docs/examples/fixtures_dict.yaml") 105 | >>> manager.get_fixture("fixture_name") 106 | {'foo': 'bar'} 107 | >>> manager.get_fixture("fixture_list") 108 | ['foo', 'bar'] 109 | 110 | .. versionadded:: 0.2.4 111 | Empty models are allowed so that dict ands lists can be used as fixtures. 112 | 113 | Getting an already existing fixture from the database 114 | ----------------------------------------------------- 115 | 116 | You can also get a fixture directly from the database (it uses ``sqlalchemy``): 117 | in this case, you just need to specify the ``model`` and an ``id``. 118 | 119 | .. literalinclude:: examples/fixtures_id.yaml 120 | :language: yaml 121 | 122 | Dependencies 123 | ------------ 124 | 125 | If a fixture depends on some side effect of another fixture, you can mark 126 | that dependency (and, necessarily, ordering) by using the ``depend_on`` 127 | section. 128 | 129 | .. literalinclude:: examples/dependencies.yaml 130 | :language: yaml 131 | 132 | .. versionadded:: 0.2.7 133 | 134 | .. _post_creation: 135 | 136 | Post creation 137 | ------------- 138 | 139 | Example: 140 | 141 | .. code-block:: yaml 142 | 143 | user: 144 | fields: 145 | name: Michel Audiard 146 | model: User 147 | post_creation: 148 | has_used_toaster: true 149 | # Note that rel are allowed in post_creation 150 | new_toaster: !rel blue_toaster 151 | 152 | For a given fixture, ``post_creation`` lets you change some attributes after 153 | instantiation. Here's the pseudo-code: 154 | 155 | .. code-block:: python 156 | 157 | instance = ObjectClass(**fields) 158 | for k, v in post_creation: 159 | setattr(instance, k, v) 160 | 161 | .. versionadded:: 0.2.0 162 | It is now possible to use ``rel`` in post_creation. 163 | 164 | Linking to other objects 165 | ------------------------ 166 | 167 | Example: 168 | 169 | .. literalinclude:: examples/relationships.yaml 170 | :language: yaml 171 | :lines: 1-16 172 | 173 | To link to another object defined in the configuration file, use ``!rel``. You 174 | can link to another objet (e.g. ``!rel toaster``) or to another object's 175 | attribute (e.g. ``!rel toaster.color``). 176 | 177 | .. doctest:: 178 | 179 | >>> manager = FixturesManager() 180 | >>> manager.load("docs/examples/relationships.yaml", 181 | ... models_package="charlatan.tests.fixtures.simple_models") 182 | >>> manager.get_fixture("user").toasters 183 | [] 184 | >>> manager.get_fixture("toaster_colors") 185 | {'color': 'red'} 186 | 187 | You can also link to specific attributes of collection's item (see 188 | :ref:`collection` for more information about collections). 189 | 190 | .. literalinclude:: examples/relationships.yaml 191 | :language: yaml 192 | :lines: 18- 193 | 194 | .. doctest:: 195 | 196 | >>> manager.get_fixture("toaster_from_collection") 197 | 198 | 199 | .. versionadded:: 0.2.0 200 | It is now possible to link to another object' attribute. 201 | 202 | .. _collection: 203 | 204 | Collections of Fixtures 205 | ----------------------- 206 | 207 | Charlatan also provides more efficient way to define variations of fixtures. 208 | The basic idea is to define the model and the default fields, then use the 209 | ``objects`` key to define related fixtures. There's two ways to define those 210 | fixtures in the ``objects`` key: 211 | 212 | * Use a list. You will then be able to access those fixtures via their index, 213 | e.g. ``toaster.0`` for the first item. 214 | * Use a dict. The key will be the name of the fixture, the value a dict of 215 | fields. You can access them via their namespace: e.g. ``toaster.blue``. 216 | 217 | You can also install all of them by installing the name of the collection. 218 | 219 | .. literalinclude:: examples/collection.yaml 220 | :language: yaml 221 | 222 | Here's how you would use this fixture file to access specific fixtures: 223 | 224 | .. doctest:: 225 | 226 | >>> manager = FixturesManager() 227 | >>> manager.load("docs/examples/collection.yaml") 228 | >>> manager.get_fixture("toasters.green") 229 | 230 | >>> manager.get_fixture("anonymous_toasters.0") 231 | 232 | 233 | You can also access the whole collection: 234 | 235 | .. doctest:: 236 | 237 | >>> pprint.pprint(manager.get_fixture("toasters")) 238 | {'blue': , 'green': } 239 | >>> manager.get_fixture("anonymous_toasters") 240 | [, ] 241 | 242 | Like any fixture, this collection can be linked to in a relationship using the 243 | ``!rel`` keyword in an intuitive way. 244 | 245 | .. doctest:: 246 | 247 | >>> pprint.pprint(manager.get_fixture("collection")) 248 | {'things': {'blue': , 'green': }} 249 | >>> user1 = manager.get_fixture("users.1") 250 | >>> user1.toasters 251 | [, ] 252 | >>> manager.get_fixture("users.2").toasters 253 | [] 254 | >>> manager.get_fixture("users.3").toasters 255 | [] 256 | 257 | .. versionchanged:: 0.3.4 258 | Access to unnamed fixture by using a ``.{index}`` notation instead of 259 | ``_{index}``. 260 | 261 | .. versionadded:: 0.3.4 262 | You can now have list of named fixtures. 263 | 264 | .. versionadded:: 0.2.8 265 | It is now possible to retrieve lists of fixtures and link to them with 266 | ``!rel`` 267 | 268 | Loading Fixtures from Multiple Files 269 | ------------------------------------ 270 | 271 | Loading fixtures from multiple files works similarly to loading collections. In 272 | this case, every fixture in a single file is preceded by a namespace taken from 273 | the name of that file. Relationships between fixtures in different files 274 | specified using the ``!rel`` keyword may be specified by prefixing the desired 275 | target fixture with its file namespace. 276 | 277 | .. literalinclude:: examples/relationships.yaml 278 | :language: yaml 279 | 280 | .. literalinclude:: examples/files.yaml 281 | :language: yaml 282 | 283 | .. doctest:: 284 | 285 | >>> manager = FixturesManager() 286 | >>> manager.load(["docs/examples/relationships.yaml", 287 | ... "docs/examples/files.yaml"], 288 | ... models_package="charlatan.tests.fixtures.simple_models") 289 | >>> manager.get_fixture("files.toaster") 290 | 291 | 292 | .. versionadded:: 0.3.7 293 | It is now possible to load multiple fixtures files with ``FixturesManager`` 294 | 295 | Datetime and timestamps 296 | ----------------------- 297 | 298 | Use ``!now``, which returns timezone-aware datetime. You can use modifiers, for 299 | instance: 300 | 301 | * ``!now +1y`` returns the current datetime plus one year 302 | * ``!now +5m`` returns the current datetime plus five months 303 | * ``!now -10d`` returns the current datetime minus ten days 304 | * ``!now +15M`` (note the case) returns the current datetime plus 15 minutes 305 | * ``!now -30s`` returns the current datetime minus 30 seconds 306 | 307 | For naive datetime (see the definition in Python's `datetime 308 | `_ module documentation), use 309 | ``!now_naive``. It also supports deltas. 310 | 311 | For Unix timestamps (seconds since the epoch) you can use ``!epoch_now``: 312 | 313 | * ``!epoch_now +1d`` returns the current datetime plus one year in seconds 314 | since the epoch 315 | * ``!epoch_now_in_ms`` returns the current timestamp in milliseconds 316 | 317 | All the same time deltas work. 318 | 319 | .. versionadded:: 0.4.6 320 | ``!epoch_now_in_ms`` was added. 321 | 322 | .. versionadded:: 0.4.4 323 | ``!now_naive`` was added. 324 | 325 | .. versionadded:: 0.2.9 326 | It is now possible to use times in seconds since the epoch 327 | 328 | Unicode Strings 329 | --------------- 330 | 331 | .. versionadded:: 0.3.5 332 | 333 | In python 2 strings are not, by default, loaded as unicode. To load all the 334 | strings from the yaml files as unicode strings, pass the option 335 | `use_unicode` as `True` when you instantiate your fixture manager. 336 | -------------------------------------------------------------------------------- /charlatan/fixture.py: -------------------------------------------------------------------------------- 1 | import copy 2 | import importlib 3 | 4 | from charlatan import _compat 5 | from charlatan.file_format import RelationshipToken 6 | from charlatan.utils import safe_iteritems, richgetter, deep_update 7 | 8 | CAN_BE_INHERITED = frozenset( 9 | ["model_name", "models_package", "fields", "post_creation", "depend_on"]) 10 | 11 | 12 | def get_class(module, klass): 13 | """Return a class object. 14 | 15 | :param str module: module path 16 | :param str klass: class name 17 | """ 18 | try: 19 | object_module = importlib.import_module(module) 20 | cls = getattr(object_module, klass) 21 | except (ImportError, TypeError) as e: 22 | raise ImportError("Unable to import %s:%s. %r" % (module, klass, e)) 23 | 24 | return cls 25 | 26 | 27 | class Inheritable(object): 28 | 29 | def __init__(self, *args, **kwargs): 30 | # This is to make sure we don't redo the inheritance twice, for 31 | # performance reason. 32 | self._has_inherited_from_parent = False 33 | self.inherit_from = None 34 | self.deep_inherit = None 35 | self.fixture_manager = None 36 | 37 | def inherit_from_parent(self): 38 | """Inherit the attributes from parent, modifying itself.""" 39 | if self._has_inherited_from_parent or not self.inherit_from: 40 | # Nothing to do 41 | return 42 | 43 | for name, value in self.get_parent_values(): 44 | setattr(self, name, value) 45 | 46 | def get_parent_values(self): 47 | """Return parent values.""" 48 | parent = self.fixture_manager.collection.get(self.inherit_from) 49 | # Recursive to make sure everything is updated. 50 | parent.inherit_from_parent() 51 | 52 | for key in CAN_BE_INHERITED: 53 | children_value = getattr(self, key) 54 | parent_value = getattr(parent, key) 55 | new_value = None 56 | if not children_value: 57 | # If I don't have a value, then we try from the parent 58 | # no matter what. 59 | new_value = copy.deepcopy(parent_value) 60 | 61 | elif isinstance(children_value, _compat.string_types): 62 | # The children value is something, then it takes 63 | # precedence no matter what. 64 | continue 65 | 66 | elif hasattr(children_value, "update"): 67 | # If it's a dict, then we try inheriting from the 68 | # parent. 69 | new_value = copy.deepcopy(parent_value) 70 | if self.deep_inherit: 71 | deep_update(new_value, children_value) 72 | else: 73 | new_value.update(children_value) 74 | 75 | if new_value: 76 | yield key, new_value 77 | 78 | 79 | class Fixture(Inheritable): 80 | 81 | """Represent a fixture that can be installed.""" 82 | 83 | def __init__(self, key, fixture_manager, 84 | model=None, fields=None, 85 | inherit_from=None, 86 | deep_inherit=False, 87 | post_creation=None, id_=None, 88 | models_package='', 89 | depend_on=frozenset()): 90 | """Create a Fixture object. 91 | 92 | :param str model: model used to instantiate the fixture, e.g. 93 | "yourlib.toaster:Toaster". If empty, the fields will be used as is. 94 | :param str models_package: default models package for relative imports 95 | :param dict fields: args to be provided when instantiating the fixture 96 | :param fixture_manager: FixturesManager creating the fixture 97 | :param dict post_creation: assignment to be done after instantiation 98 | :param str inherit_from: model to inherit from 99 | :param bool deep_inherit: if True, fields will support nested updates 100 | :param list depend_on: A list of relationships to depend on 101 | 102 | .. versionadded:: 0.4.5 103 | ``deep_inherit`` argument added. 104 | 105 | .. versionadded:: 0.4.0 106 | ``models_package`` argument added. 107 | 108 | """ 109 | super(Fixture, self).__init__() 110 | 111 | if id_ and fields: 112 | raise ValueError( 113 | "Cannot provide both id and fields to create fixture.") 114 | 115 | self.key = key 116 | self.fixture_manager = fixture_manager 117 | 118 | self.database_id = id_ 119 | self.inherit_from = inherit_from 120 | self.deep_inherit = deep_inherit 121 | 122 | # Stuff that can be inherited. 123 | self.model_name = model 124 | self.models_package = models_package 125 | self.fields = fields or {} 126 | self.post_creation = post_creation or {} 127 | self.depend_on = depend_on 128 | 129 | def __repr__(self): 130 | return "" % self.key 131 | 132 | def get_instance(self, path=None, overrides=None, builder=None): 133 | """Instantiate the fixture using the model and return the instance. 134 | 135 | :param str path: remaining path to return 136 | :param dict overrides: overriding fields 137 | :param func builder: function that is used to get the fixture 138 | 139 | .. deprecated:: 0.4.0 140 | ``fields`` argument renamed ``overrides``. 141 | 142 | .. versionadded:: 0.4.0 143 | ``builder`` argument added. 144 | 145 | .. deprecated:: 0.3.7 146 | ``include_relationships`` argument removed. 147 | 148 | """ 149 | self.inherit_from_parent() # Does the modification in place. 150 | 151 | if self.database_id: 152 | object_class = self.get_class() 153 | # No need to create a new object, just get it from the db 154 | instance = ( 155 | self.fixture_manager.session.query(object_class) 156 | .get(self.database_id) 157 | ) 158 | 159 | else: 160 | # We need to do a copy since we're modifying them. 161 | params = copy.deepcopy(self.fields) 162 | if overrides: 163 | params.update(overrides) 164 | 165 | for key, value in safe_iteritems(params): 166 | if callable(value): 167 | params[key] = value() 168 | 169 | # Get the class 170 | object_class = self.get_class() 171 | 172 | # Does not return anything, does the modification in place (in 173 | # fields). 174 | self._process_relationships(params) 175 | 176 | if object_class: 177 | instance = builder(self.fixture_manager, object_class, params) 178 | else: 179 | # Return the fields as is. This allows to enter dicts 180 | # and lists directly. 181 | instance = params 182 | 183 | # Do any extra assignment 184 | for attr, value in self.post_creation.items(): 185 | if isinstance(value, RelationshipToken): 186 | value = self.get_relationship(value) 187 | 188 | setattr(instance, attr, value) 189 | 190 | if path: 191 | return richgetter(instance, path) 192 | else: 193 | return instance 194 | 195 | def get_class(self): 196 | """Return class object for this instance.""" 197 | if not self.model_name: 198 | return 199 | 200 | # Relative path, e.g. ".toaster:Toaster" 201 | if ":" in self.model_name and self.model_name[0] == ".": 202 | module, klass = self.model_name.split(":") 203 | module = self.models_package + module 204 | return get_class(module, klass) 205 | 206 | # Absolute import, e.g. "yourlib.toaster:Toaster" 207 | if ":" in self.model_name: 208 | module, klass = self.model_name.split(":") 209 | return get_class(module, klass) 210 | 211 | # Class alone, e.g. "Toaster". 212 | # Trying to import from e.g. yourlib.toaster:Toaster 213 | module = "{models_package}.{model}".format( 214 | models_package=self.models_package, 215 | model=self.model_name.lower()) 216 | klass = self.model_name 217 | 218 | try: 219 | return get_class(module, klass) 220 | except ImportError: 221 | # Then try to import from yourlib:Toaster 222 | return get_class(self.models_package, klass) 223 | 224 | @staticmethod 225 | def extract_rel_name(name): 226 | """Return the relationship and attr from an argument to !rel.""" 227 | rel_name = name # e.g. toaster.color 228 | attr = None 229 | 230 | # TODO: we support only one level for now 231 | if "." in name: 232 | path = name.split(".") 233 | rel_name, attr = path[0], path[1] 234 | return rel_name, attr 235 | 236 | def extract_relationships(self): 237 | """Return all dependencies. 238 | 239 | :rtype generator: 240 | 241 | Yields ``(depends_on, attr_name)``. 242 | 243 | """ 244 | # TODO: make this DRYer since it's mostly copied from 245 | # _process_relationships 246 | 247 | for dep in self.depend_on: 248 | yield dep, None 249 | 250 | for name, value in safe_iteritems(self.fields): 251 | # One to one relationship 252 | if isinstance(value, RelationshipToken): 253 | yield self.extract_rel_name(value) 254 | 255 | # One to many relationship 256 | elif isinstance(value, (tuple, list)): 257 | for i, nested_value in enumerate(value): 258 | if isinstance(nested_value, RelationshipToken): 259 | yield self.extract_rel_name(nested_value) 260 | 261 | def _process_field_relationships(self, field_value): 262 | """Create any relationship for a field if needed. 263 | 264 | :param mixed field_value: field value to be processed 265 | 266 | For each field that is a relationship or a list of relationships, 267 | instantiate those relationships and update the fields. 268 | 269 | Returns new field value and modifies lists in place. 270 | """ 271 | # One to one relationship 272 | if isinstance(field_value, RelationshipToken): 273 | return self.get_relationship(field_value) 274 | 275 | # One to many relationship 276 | elif isinstance(field_value, (tuple, list)): 277 | for i, nested_value in enumerate(field_value): 278 | field_value[i] = self._process_field_relationships( 279 | nested_value) 280 | 281 | return field_value 282 | 283 | def _process_relationships(self, fields): 284 | """Create any relationship if needed. 285 | 286 | :param dict fields: fields to be processed 287 | 288 | For each field that is a relationship or a list of relationships, 289 | instantiate those relationships and update the fields. 290 | 291 | Does not return anything, modify fields in place. 292 | """ 293 | # For dictionaries, iterate over key, value and for lists iterate over 294 | # index, item 295 | if hasattr(fields, 'items'): 296 | field_iterator = _compat.iteritems(fields) 297 | else: 298 | field_iterator = enumerate(fields) 299 | 300 | for name, value in field_iterator: 301 | fields[name] = self._process_field_relationships(value) 302 | 303 | def get_relationship(self, name): 304 | """Get a relationship and its attribute if necessary.""" 305 | # This function is needed so that this fixture can require other 306 | # fixtures. If a fixture requires another fixture, it 307 | # necessarily means that it needs to include other relationships 308 | # as well. 309 | return self.fixture_manager.get_fixture(name) 310 | -------------------------------------------------------------------------------- /charlatan/fixtures_manager.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | from glob import glob 3 | from itertools import chain 4 | import functools 5 | import os 6 | 7 | from charlatan import _compat 8 | from charlatan import builder 9 | from charlatan.depgraph import DepGraph 10 | from charlatan.file_format import load_file 11 | from charlatan.fixture import Fixture 12 | from charlatan import fixture_collection 13 | 14 | ALLOWED_HOOKS = ("before_save", "after_save", "before_install", 15 | "after_install") 16 | ROOT_COLLECTION = "root" 17 | 18 | 19 | def make_list(obj): 20 | """Return list of objects if necessary.""" 21 | if isinstance(obj, _compat.string_types): 22 | return (obj, ) 23 | return obj 24 | 25 | 26 | class FixturesManager(object): 27 | 28 | """ 29 | Manage Fixture objects. 30 | 31 | :param Session db_session: sqlalchemy Session object 32 | :param bool use_unicode: 33 | :param func get_builder: 34 | :param func delete_builder: 35 | 36 | .. versionadded:: 0.4.0 37 | ``get_builder`` and ``delete_builder`` arguments were added. 38 | 39 | .. deprecated:: 0.4.0 40 | ``delete_instance``, ``save_instance`` methods were deleted in favor 41 | of using builders. 42 | 43 | .. versionadded:: 0.3.0 44 | ``db_session`` argument was added. 45 | """ 46 | 47 | DictFixtureCollection = fixture_collection.DictFixtureCollection 48 | ListFixtureCollection = fixture_collection.ListFixtureCollection 49 | 50 | default_get_builder = builder.InstantiateAndSave() 51 | default_delete_builder = builder.DeleteAndCommit() 52 | 53 | def __init__(self, db_session=None, use_unicode=False, 54 | get_builder=None, delete_builder=None, 55 | ): 56 | self.hooks = {} 57 | self.session = db_session 58 | self.installed_keys = [] 59 | self.use_unicode = use_unicode 60 | self.get_builder = get_builder or self.default_get_builder 61 | self.delete_builder = delete_builder or self.default_delete_builder 62 | self.filenames = [] 63 | self.collection = self.DictFixtureCollection( 64 | ROOT_COLLECTION, 65 | fixture_manager=self, 66 | ) 67 | 68 | def load(self, filenames, models_package=""): 69 | """Pre-load the fixtures. Does not install anything. 70 | 71 | :param list_or_str filename: file or list of files that holds the 72 | fixture data 73 | :param str models_package: package holding the models definition 74 | 75 | .. deprecated:: 0.3.0 76 | ``db_session`` argument was removed and put in the object's 77 | constructor arguments. 78 | 79 | .. versionchanged:: 0.3.7 80 | ``filename`` argument was changed to ``filenames``, which can be 81 | list or string. 82 | 83 | """ 84 | self.filenames.append(filenames) 85 | 86 | self.depgraph = self._load_fixtures(filenames, 87 | models_package=models_package) 88 | self.clean_cache() 89 | 90 | def _get_namespace_from_filename(self, filename): 91 | """Get a collection namespace from a fixtures filename. 92 | 93 | :param str filename: filename to extract namespace from 94 | """ 95 | segments = os.path.basename(filename).split(".") 96 | if len(segments) > 2: 97 | raise ValueError("Fixtures filename stem may not contain periods") 98 | 99 | return segments[0] 100 | 101 | def _load_fixtures(self, filenames, models_package=''): 102 | """Pre-load the fixtures. 103 | 104 | :param list or str filenames: files that hold the fixture data 105 | :param str models_package: 106 | """ 107 | if isinstance(filenames, _compat.string_types): 108 | globbed_filenames = glob(filenames) 109 | else: 110 | globbed_filenames = list( 111 | chain.from_iterable(glob(f) for f in filenames) 112 | ) 113 | 114 | if not globbed_filenames: 115 | raise IOError('File "%s" not found' % filenames) 116 | 117 | if len(globbed_filenames) == 1: 118 | content = load_file(globbed_filenames[0], self.use_unicode) 119 | else: 120 | content = {} 121 | 122 | for filename in globbed_filenames: 123 | namespace = self._get_namespace_from_filename(filename) 124 | content[namespace] = { 125 | "objects": load_file(filename, self.use_unicode) 126 | } 127 | 128 | if content: 129 | for k, v in _compat.iteritems(content): 130 | 131 | if "objects" in v: 132 | # It's a collection of fictures. 133 | collection = self._handle_collection( 134 | namespace=k, 135 | definition=v, 136 | objects=v["objects"], 137 | models_package=models_package, 138 | ) 139 | self.collection.add(k, collection) 140 | 141 | # Named fixtures 142 | else: 143 | if "id" in v: 144 | # Renaming id because it's a Python builtin function 145 | v["id_"] = v["id"] 146 | del v["id"] 147 | 148 | fixture = Fixture( 149 | key=k, 150 | fixture_manager=self, 151 | models_package=models_package, 152 | **v) 153 | self.collection.add(k, fixture) 154 | 155 | graph = self._check_cycle(self.collection) 156 | return graph 157 | 158 | def _check_cycle(self, collection): 159 | """Raise an exception if there's a relationship cycle.""" 160 | d = DepGraph() 161 | for _, fixture in collection: 162 | for dependency, _ in fixture.extract_relationships(): 163 | d.add_edge(dependency, fixture.key) 164 | 165 | # This does nothing except raise an error if there's a cycle 166 | d.topo_sort() 167 | return d 168 | 169 | def _handle_collection(self, namespace, definition, objects, 170 | models_package=''): 171 | """Handle a collection of fixtures. 172 | 173 | :param dict definition: definition of the collection 174 | :param dict_or_list objects: fixtures in the collection 175 | :param str models_package: 176 | 177 | """ 178 | if isinstance(objects, list): 179 | klass = self.ListFixtureCollection 180 | else: 181 | klass = self.DictFixtureCollection 182 | 183 | collection = klass( 184 | key=namespace, 185 | fixture_manager=self, 186 | model=definition.get('model'), 187 | models_package=definition.get('models_package'), 188 | fields=definition.get('fields'), 189 | post_creation=definition.get('post_creation'), 190 | inherit_from=definition.get('inherit_from'), 191 | depend_on=definition.get('depend_on'), 192 | ) 193 | 194 | for name, new_fields in collection.iterator(objects): 195 | qualified_name = "%s.%s" % (namespace, name) 196 | 197 | if "objects" in new_fields: 198 | # A nested collection, either because we're dealing with a file 199 | # collection or a sub-collection. 200 | fixture = self._handle_collection( 201 | namespace=qualified_name, 202 | definition=new_fields, 203 | objects=new_fields["objects"] 204 | ) 205 | else: 206 | model = new_fields.pop("model", None) 207 | # In the case of a file collection we'll be dealing with 208 | # PyYAML's output from that file, which means that individual 209 | # fixtures in this collection have the "fields" field. 210 | fields = new_fields.pop("fields", new_fields) 211 | inherit_from = namespace if model is None else None 212 | 213 | fixture = Fixture( 214 | key=qualified_name, 215 | fixture_manager=self, 216 | # Automatically inherit from the collection 217 | inherit_from=inherit_from, 218 | fields=fields, 219 | model=model, 220 | models_package=models_package, 221 | # The rest (default fields, etc.) is 222 | # automatically inherited from the collection. 223 | ) 224 | collection.add(name, fixture) 225 | 226 | return collection 227 | 228 | def clean_cache(self): 229 | """Clean the cache.""" 230 | self.cache = {} 231 | self.installed_keys = [] 232 | 233 | def delete_fixture(self, fixture_key, builder=None): 234 | """Delete a fixture instance. 235 | 236 | :param str fixture_key: 237 | :param func builder: 238 | 239 | Before and after the process, the :func:`before_delete` and 240 | :func:`after_delete` hook are run. 241 | 242 | .. versionadded:: 0.4.0 243 | ``builder`` argument was added. 244 | 245 | .. deprecated:: 0.4.0 246 | ``delete_instance`` method renamed to ``delete_fixture`` for 247 | consistency reason. 248 | """ 249 | builder = builder or self.delete_builder 250 | self.get_hook("before_delete")(fixture_key) 251 | 252 | instance = self.cache.get(fixture_key) 253 | if instance: 254 | self.cache.pop(fixture_key, None) 255 | self.installed_keys.remove(fixture_key) 256 | builder(self, instance) 257 | 258 | self.get_hook("after_delete")(fixture_key) 259 | 260 | def install_fixture(self, fixture_key, overrides=None): 261 | """Install a fixture. 262 | 263 | :param str fixture_key: 264 | :param dict overrides: override fields 265 | 266 | :rtype: :data:`fixture_instance` 267 | 268 | .. deprecated:: 0.4.0 269 | ``do_not_save`` argument was removed. 270 | ``attrs`` argument renamed ``overrides``. 271 | 272 | .. deprecated:: 0.3.7 273 | ``include_relationships`` argument was removed. 274 | 275 | """ 276 | builder = functools.partial(self.get_builder, 277 | save=True, 278 | session=self.session) 279 | self.get_hook("before_install")() 280 | 281 | try: 282 | instance = self.get_fixture(fixture_key, overrides=overrides, 283 | builder=builder) 284 | except Exception as exc: 285 | self.get_hook("after_install")(exc) 286 | raise 287 | 288 | else: 289 | self.get_hook("after_install")(None) 290 | return instance 291 | 292 | def install_fixtures(self, fixture_keys): 293 | """Install a list of fixtures. 294 | 295 | :param fixture_keys: fixtures to be installed 296 | :type fixture_keys: str or list of strs 297 | :rtype: list of :data:`fixture_instance` 298 | 299 | .. deprecated:: 0.4.0 300 | ``do_not_save`` argument was removed. 301 | 302 | .. deprecated:: 0.3.7 303 | ``include_relationships`` argument was removed. 304 | 305 | """ 306 | instances = [] 307 | for f in make_list(fixture_keys): 308 | instances.append(self.install_fixture(f)) 309 | return instances 310 | 311 | def install_all_fixtures(self): 312 | """Install all fixtures. 313 | 314 | :rtype: list of :data:`fixture_instance` 315 | 316 | .. deprecated:: 0.4.0 317 | ``do_not_save`` argument was removed. 318 | 319 | .. deprecated:: 0.3.7 320 | ``include_relationships`` argument was removed. 321 | 322 | """ 323 | return self.install_fixtures(self.keys()) 324 | 325 | def uninstall_fixture(self, fixture_key): 326 | """Uninstall a fixture. 327 | 328 | :param str fixture_key: 329 | :rtype: ``None`` 330 | 331 | .. deprecated:: 0.4.0 332 | ``do_not_delete`` argument was removed. This function does not 333 | return anything. 334 | """ 335 | builder = functools.partial(self.delete_builder, 336 | commit=True, 337 | session=self.session) 338 | return self.delete_fixture(fixture_key, builder) 339 | 340 | def uninstall_fixtures(self, fixture_keys): 341 | """Uninstall a list of installed fixtures. 342 | 343 | :param fixture_keys: fixtures to be uninstalled 344 | :type fixture_keys: str or list of strs 345 | :rtype: ``None`` 346 | 347 | .. deprecated:: 0.4.0 348 | ``do_not_delete`` argument was removed. This function does not 349 | return anything. 350 | """ 351 | for fixture_key in make_list(fixture_keys): 352 | self.uninstall_fixture(fixture_key) 353 | 354 | def uninstall_all_fixtures(self): 355 | """Uninstall all installed fixtures. 356 | 357 | :rtype: ``None`` 358 | 359 | .. deprecated:: 0.4.0 360 | ``do_not_delete`` argument was removed. This function does not 361 | return anything. 362 | """ 363 | installed_fixtures = list(self.installed_keys) 364 | installed_fixtures.reverse() 365 | self.uninstall_fixtures(installed_fixtures) 366 | 367 | def keys(self): 368 | """Return all fixture keys.""" 369 | return self.collection.fixtures.keys() 370 | 371 | def get_fixture(self, fixture_key, overrides=None, builder=None): 372 | """Return a fixture instance (but do not save it). 373 | 374 | :param str fixture_key: 375 | :param dict overrides: override fields 376 | :param func builder: build builder. 377 | :rtype: instantiated but unsaved fixture 378 | 379 | .. versionadded:: 0.4.0 380 | ``builder`` argument was added. 381 | ``attrs`` argument renamed ``overrides``. 382 | 383 | .. deprecated:: 0.4.0 384 | ``do_not_save`` argument was removed. 385 | 386 | .. deprecated:: 0.3.7 387 | ``include_relationships`` argument was removed. 388 | """ 389 | builder = builder or self.get_builder 390 | # initialize all parents in topological order 391 | parents = [] 392 | for fixture in self.depgraph.ancestors_of(fixture_key): 393 | parents.append(self.get_fixture(fixture, builder=builder)) 394 | 395 | # Fixture are cached so that setting up relationships is not too 396 | # expensive. We don't get the cached version if overrides are 397 | # overriden. 398 | returned = None 399 | if not overrides: 400 | returned = self.cache.get(fixture_key) 401 | 402 | if not returned: 403 | returned = self.collection.get_instance( 404 | fixture_key, overrides=overrides, builder=builder) 405 | 406 | self.cache[fixture_key] = returned 407 | self.installed_keys.append(fixture_key) 408 | 409 | return returned 410 | 411 | def get_fixtures(self, fixture_keys, builder=None): 412 | """Get fixtures from iterable. 413 | 414 | :param iterable fixture_keys: 415 | :rtype: list of instantiated but unsaved fixtures 416 | 417 | .. versionadded:: 0.4.0 418 | ``builder`` argument was added. 419 | 420 | .. deprecated:: 0.3.7 421 | ``include_relationships`` argument was removed. 422 | """ 423 | builder = builder or self.get_builder 424 | fixtures = [] 425 | for f in fixture_keys: 426 | fixtures.append(self.get_fixture(f, builder=builder)) 427 | return fixtures 428 | 429 | def get_all_fixtures(self, builder=None): 430 | """Get all fixtures. 431 | 432 | :param iterable fixture_keys: 433 | :rtype: list of instantiated but unsaved fixtures 434 | 435 | .. versionadded:: 0.4.0 436 | ``builder`` argument was added. 437 | 438 | .. deprecated:: 0.3.7 439 | ``include_relationships`` argument was removed. 440 | """ 441 | builder = builder or self.get_builder 442 | return self.get_fixtures(self.keys(), builder=builder) 443 | 444 | def get_hook(self, hook_name): 445 | """Return a hook. 446 | 447 | :param str hook_name: e.g. ``before_delete``. 448 | """ 449 | if hook_name in self.hooks: 450 | return self.hooks[hook_name] 451 | 452 | return lambda *args: None 453 | 454 | def set_hook(self, hookname, func): 455 | """Add a hook. 456 | 457 | :param str hookname: 458 | :param function func: 459 | """ 460 | if hookname not in ALLOWED_HOOKS: 461 | raise KeyError("'%s' is not an allowed hook." % hookname) 462 | 463 | self.hooks[hookname] = func 464 | --------------------------------------------------------------------------------