From db643d44c51659d9460aa73eb21e7888b22896de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sat, 17 Mar 2012 02:29:24 +0100 Subject: Add a '@use_strategy' decorator for forcing alternate strategies. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Raphaël Barrois --- factory/__init__.py | 1 + factory/base.py | 11 +++++++++++ 2 files changed, 12 insertions(+) (limited to 'factory') diff --git a/factory/__init__.py b/factory/__init__.py index 1d4408f..dd91343 100644 --- a/factory/__init__.py +++ b/factory/__init__.py @@ -44,6 +44,7 @@ from base import ( BUILD_STRATEGY, CREATE_STRATEGY, STUB_STRATEGY, + use_strategy, DJANGO_CREATION, NAIVE_BUILD, diff --git a/factory/base.py b/factory/base.py index 62131fb..ef782c7 100644 --- a/factory/base.py +++ b/factory/base.py @@ -614,3 +614,14 @@ def simple_generate(klass, create, **kwargs): def simple_generate_batch(klass, create, size, **kwargs): """Create a factory for the given class, and simple_generate instances.""" return make_factory(klass, **kwargs).simple_generate_batch(create, size) + + +def use_strategy(new_strategy): + """Force the use of a different strategy. + + This is an alternative to setting default_strategy in the class definition. + """ + def wrapped_class(klass): + klass.default_strategy = new_strategy + return klass + return wrapped_class -- cgit v1.2.3 From 4bcf146c71589f4b225823eb418ee6908b2efb6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sat, 7 Apr 2012 02:14:58 +0200 Subject: Add and document abstract factories (Closes #8). MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Raphaël Barrois --- factory/base.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) (limited to 'factory') diff --git a/factory/base.py b/factory/base.py index ef782c7..0edf961 100644 --- a/factory/base.py +++ b/factory/base.py @@ -92,9 +92,8 @@ class BaseFactoryMetaClass(type): """ parent_factories = get_factory_bases(bases) - if not parent_factories or attrs.get('ABSTRACT_FACTORY', False): - # If this isn't a subclass of Factory, or specifically declared - # abstract, don't do anything special. + if not parent_factories: + # If this isn't a subclass of Factory, don't do anything special. return super(BaseFactoryMetaClass, cls).__new__(cls, class_name, bases, attrs) declarations = containers.DeclarationDict() @@ -200,7 +199,11 @@ class FactoryMetaClass(BaseFactoryMetaClass): parent_factories = get_factory_bases(bases) if not parent_factories or attrs.get('ABSTRACT_FACTORY', False): - # If this isn't a subclass of Factory, don't do anything special. + # If this isn't a subclass of Factory, or specifically declared + # abstract, don't do anything special. + if 'ABSTRACT_FACTORY' in attrs: + attrs.pop('ABSTRACT_FACTORY') + return super(FactoryMetaClass, cls).__new__(cls, class_name, bases, attrs) base = parent_factories[0] -- cgit v1.2.3 From ff958948b937386d3c8581dbe0dc368e5796c15a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sat, 7 Apr 2012 02:15:40 +0200 Subject: Fix the __str__ method of factory classes. --- factory/base.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) (limited to 'factory') diff --git a/factory/base.py b/factory/base.py index 0edf961..4ccda9e 100644 --- a/factory/base.py +++ b/factory/base.py @@ -227,6 +227,9 @@ class FactoryMetaClass(BaseFactoryMetaClass): return super(FactoryMetaClass, cls).__new__(cls, class_name, bases, attrs, extra_attrs=extra_attrs) + def __str__(self): + return '<%s for %s>' % (self.__name__, + getattr(self, CLASS_ATTRIBUTE_ASSOCIATED_CLASS).__name__) # Factory base classes @@ -454,10 +457,6 @@ class Factory(BaseFactory): # from turning it into an instance method. _creation_function = (DJANGO_CREATION,) - def __str__(self): - return '<%s for %s>' % (self.__class__.__name__, - getattr(self, CLASS_ATTRIBUTE_ASSOCIATED_CLASS).__name__) - @classmethod def set_creation_function(cls, creation_function): """Set the creation function for this class. -- cgit v1.2.3 From e80cbdc3224297ee57667e4000f1a671af05f520 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Mon, 9 Apr 2012 01:10:42 +0200 Subject: Move ATTR_SPLITTER logic to a dedicated module. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Raphaël Barrois --- factory/containers.py | 19 ++------------ factory/utils.py | 72 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+), 17 deletions(-) create mode 100644 factory/utils.py (limited to 'factory') diff --git a/factory/containers.py b/factory/containers.py index 2f92f62..b8557d6 100644 --- a/factory/containers.py +++ b/factory/containers.py @@ -22,11 +22,7 @@ from factory import declarations - - -#: String for splitting an attribute name into a -#: (subfactory_name, subfactory_field) tuple. -ATTR_SPLITTER = '__' +from factory import utils class CyclicDefinitionError(Exception): @@ -238,18 +234,7 @@ class AttributeBuilder(object): self.factory = factory self._containers = extra.pop('__containers', None) self._attrs = factory.declarations(extra) - self._subfields = self._extract_subfields() - - def _extract_subfields(self): - """Extract the subfields from the declarations list.""" - sub_fields = {} - for key in list(self._attrs): - if ATTR_SPLITTER in key: - # Trying to define a default of a subfactory - cls_name, attr_name = key.split(ATTR_SPLITTER, 1) - if cls_name in self._attrs: - sub_fields.setdefault(cls_name, {})[attr_name] = self._attrs.pop(key) - return sub_fields + self._subfields = utils.multi_extract_dict(self._attrs.keys(), self._attrs) def build(self, create): """Build a dictionary of attributes. diff --git a/factory/utils.py b/factory/utils.py new file mode 100644 index 0000000..6c6fd7d --- /dev/null +++ b/factory/utils.py @@ -0,0 +1,72 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2010 Mark Sandstrom +# Copyright (c) 2011 Raphaël Barrois +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + + +#: String for splitting an attribute name into a +#: (subfactory_name, subfactory_field) tuple. +ATTR_SPLITTER = '__' + +def extract_dict(prefix, kwargs, pop=True): + """Extracts all values beginning with a given prefix from a dict. + + Can either 'pop' or 'get' them; + + Args: + prefix (str): the prefix to use for lookups + kwargs (dict): the dict from which values should be extracted + pop (bool): whether to use pop (True) or get (False) + + Returns: + A new dict, containing values from kwargs and beginning with + prefix + ATTR_SPLITTER. That full prefix is removed from the keys + of the returned dict. + """ + prefix = prefix + ATTR_SPLITTER + extracted = {} + for key in kwargs.keys(): + if key.startswith(prefix): + new_key = key[len(prefix):] + if pop: + value = kwargs.pop(key) + else: + value = kwargs[key] + extracted[new_key] = value + return extracted + + +def declength_compare(a, b): + """Compare objects, choosing longest first.""" + if len(a) > len(b): + return -1 + elif len(a) < len(b): + return 1 + else: + return cmp(a, b) + + +def multi_extract_dict(prefixes, kwargs, pop=True): + """Extracts all values from a given list of prefixes.""" + results = {} + for prefix in sorted(prefixes, cmp=declength_compare): + extracted = extract_dict(prefix, kwargs, pop=pop) + results[prefix] = extracted + return results -- cgit v1.2.3 From 184dd0516267c58370d6a88afb1c1ce894b2b7c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Mon, 9 Apr 2012 13:31:13 +0200 Subject: Cleaner extract_dict: allow excluded keys. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Raphaël Barrois --- factory/utils.py | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) (limited to 'factory') diff --git a/factory/utils.py b/factory/utils.py index 6c6fd7d..2fcd7ff 100644 --- a/factory/utils.py +++ b/factory/utils.py @@ -25,7 +25,7 @@ #: (subfactory_name, subfactory_field) tuple. ATTR_SPLITTER = '__' -def extract_dict(prefix, kwargs, pop=True): +def extract_dict(prefix, kwargs, pop=True, exclude=()): """Extracts all values beginning with a given prefix from a dict. Can either 'pop' or 'get' them; @@ -34,6 +34,7 @@ def extract_dict(prefix, kwargs, pop=True): prefix (str): the prefix to use for lookups kwargs (dict): the dict from which values should be extracted pop (bool): whether to use pop (True) or get (False) + exclude (iterable): list of prefixed keys that shouldn't be extracted Returns: A new dict, containing values from kwargs and beginning with @@ -43,6 +44,9 @@ def extract_dict(prefix, kwargs, pop=True): prefix = prefix + ATTR_SPLITTER extracted = {} for key in kwargs.keys(): + if key in exclude: + continue + if key.startswith(prefix): new_key = key[len(prefix):] if pop: @@ -63,10 +67,21 @@ def declength_compare(a, b): return cmp(a, b) -def multi_extract_dict(prefixes, kwargs, pop=True): - """Extracts all values from a given list of prefixes.""" +def multi_extract_dict(prefixes, kwargs, pop=True, exclude=()): + """Extracts all values from a given list of prefixes. + + Arguments have the same meaning as for extract_dict. + + Returns: + dict(str => dict): a dict mapping each prefix to the dict of extracted + key/value. + """ results = {} + exclude = list(exclude) for prefix in sorted(prefixes, cmp=declength_compare): - extracted = extract_dict(prefix, kwargs, pop=pop) + extracted = extract_dict(prefix, kwargs, pop=pop, exclude=exclude) results[prefix] = extracted + exclude.extend( + ['%s%s%s' % (prefix, ATTR_SPLITTER, key) for key in extracted]) + return results -- cgit v1.2.3 From 3ba8ed544fa9e866f97efc41155ee296f022e9b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Fri, 13 Apr 2012 17:51:01 +0200 Subject: Add basis for PostGenerationDeclaration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Raphaël Barrois --- factory/declarations.py | 113 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 113 insertions(+) (limited to 'factory') diff --git a/factory/declarations.py b/factory/declarations.py index 41d99a3..d9d560a 100644 --- a/factory/declarations.py +++ b/factory/declarations.py @@ -250,6 +250,119 @@ class SubFactory(OrderedDeclaration): return self.factory.build(**attrs) +class PostGenerationDeclaration(object): + """Declarations to be called once the target object has been generated. + + Attributes: + extract_prefix (str): prefix to use when extracting attributes from + the factory's declaration for this declaration. If empty, uses + the attribute name of the PostGenerationDeclaration. + """ + + def __init__(self, extract_prefix=None): + self.extract_prefix = extract_prefix + + def extract(self, name, attrs): + """Extract relevant attributes from a dict. + + Args: + name (str): the name at which this PostGenerationDeclaration was + defined in the declarations + attrs (dict): the attribute dict from which values should be + extracted + + Returns: + (object, dict): a tuple containing the attribute at 'name' (if + provided) and a dict of extracted attributes + """ + extracted = attrs.get(name) + if self.extract_prefix: + extract_prefix = self.extract_prefix + else: + extract_prefix = name + kwargs = utils.extract_dict(extract_prefix) + return extracted, kwargs + + def call(self, obj, create, extracted=None, **kwargs): + """Call this hook; no return value is expected. + + Args: + obj (object): the newly generated object + create (bool): whether the object was 'built' or 'created' + extracted (object): the value given for in the + object definition, or None if not provided. + kwargs (dict): declarations extracted from the object + definition for this hook + """ + raise NotImplementedError() + + +class PostGeneration(PostGenerationDeclaration): + """Calls a given function once the object has been generated.""" + def __init__(self, function, extract_prefix=None): + super(PostGeneration, self).__init__(extract_prefix) + self.function = function + + def call(self, obj, create, extracted=None, **kwargs): + self.function(obj, create, extracted, **kwargs) + + +def post_declaration(extract_prefix=None): + def decorator(fun): + return PostGeneration(fun, extract_prefix=extract_prefix) + return decorator + + +class RelatedFactory(PostGenerationDeclaration): + """Calls a factory once the object has been generated. + + Attributes: + factory (Factory): the factory to call + defaults (dict): extra declarations for calling the related factory + name (str): the name to use to refer to the generated object when + calling the related factory + """ + + def __init__(self, factory, name='', **defaults): + super(RelatedFactory, self).__init__(extract_prefix=None) + self.factory = factory + self.name = name + self.defaults = defaults + + def call(self, obj, create, extracted=None, **kwargs): + passed_kwargs = dict(self.defaults) + passed_kwargs.update(kwargs) + if self.name: + passed_kwargs[self.name] = obj + self.factory.simple_generate(create, **passed_kwargs) + + +class PostGenerationMethodCall(PostGenerationDeclaration): + """Calls a method of the generated object. + + Attributes: + method_name (str): the method to call + method_args (list): arguments to pass to the method + method_kwargs (dict): keyword arguments to pass to the method + + Example: + class UserFactory(factory.Factory): + ... + password = factory.PostGenerationMethodCall('set_password', password='') + """ + def __init__(self, method_name, extract_prefix=None, *args, **kwargs): + super(RelatedFactory, self).__init__(extract_prefix) + self.method_name = method_name + self.method_args = args + self.method_kwargs = kwargs + + def call(self, obj, create, extracted=None, **kwargs): + passed_kwargs = dict(self.method_kwargs) + passed_kwargs.update(kwargs) + method = getattr(obj, self.method_name) + method(*self.method_args, **passed_kwargs) + + # Decorators... in case lambdas don't cut it def lazy_attribute(func): -- cgit v1.2.3 From 27dcb6fe29a09d6cb2d83a59229e562093da4f7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Fri, 13 Apr 2012 19:33:22 +0200 Subject: Whitespace cleanup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Raphaël Barrois --- factory/base.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'factory') diff --git a/factory/base.py b/factory/base.py index 4ccda9e..98029e3 100644 --- a/factory/base.py +++ b/factory/base.py @@ -320,7 +320,7 @@ class BaseFactory(object): Args: size (int): the number of instances to build - + Returns: object list: the built instances """ @@ -337,7 +337,7 @@ class BaseFactory(object): Args: size (int): the number of instances to create - + Returns: object list: the created instances """ @@ -361,7 +361,7 @@ class BaseFactory(object): Args: size (int): the number of instances to stub - + Returns: object list: the stubbed instances """ -- cgit v1.2.3 From b590e5014351a79d66d2f4816b1a6aa83908f395 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Fri, 13 Apr 2012 19:33:54 +0200 Subject: Add PostGenerationDeclarationDict (cf. DeclarationDict). MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Raphaël Barrois --- factory/containers.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) (limited to 'factory') diff --git a/factory/containers.py b/factory/containers.py index b8557d6..9f480cc 100644 --- a/factory/containers.py +++ b/factory/containers.py @@ -149,13 +149,21 @@ class DeclarationDict(dict): Args: extra (dict): additional attributes to include in the copy. """ - new = DeclarationDict() + new = self.__class__() new.update(self) if extra: new.update(extra) return new +class PostGenerationDeclarationDict(DeclarationDict): + """Alternate DeclarationDict for PostGenerationDeclaration.""" + + def is_declaration(self, name, value): + """Captures instances of PostGenerationDeclaration.""" + return isinstance(value, declarations.PostGenerationDeclaration) + + class LazyValue(object): """Some kind of "lazy evaluating" object.""" -- cgit v1.2.3 From 0e7fed312bf2de6d628a61b116bd91e04bf0a9ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Fri, 13 Apr 2012 19:42:19 +0200 Subject: Handle the PostGeneration declarations. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Raphaël Barrois --- factory/__init__.py | 1 + factory/base.py | 44 ++++++++++++++++++++++++++++++++++---------- factory/declarations.py | 6 ++++-- 3 files changed, 39 insertions(+), 12 deletions(-) (limited to 'factory') diff --git a/factory/__init__.py b/factory/__init__.py index dd91343..1bf8968 100644 --- a/factory/__init__.py +++ b/factory/__init__.py @@ -67,5 +67,6 @@ from declarations import ( sequence, lazy_attribute_sequence, container_attribute, + post_declaration, ) diff --git a/factory/base.py b/factory/base.py index 98029e3..c1dbd98 100644 --- a/factory/base.py +++ b/factory/base.py @@ -44,6 +44,7 @@ FACTORY_CLASS_DECLARATION = 'FACTORY_FOR' # Factory class attributes CLASS_ATTRIBUTE_DECLARATIONS = '_declarations' +CLASS_ATTRIBUTE_POSTGEN_DECLARATIONS = '_postgen_declarations' CLASS_ATTRIBUTE_ASSOCIATED_CLASS = '_associated_class' @@ -97,18 +98,24 @@ class BaseFactoryMetaClass(type): return super(BaseFactoryMetaClass, cls).__new__(cls, class_name, bases, attrs) declarations = containers.DeclarationDict() + postgen_declarations = containers.PostGenerationDeclarationDict() # Add parent declarations in reverse order. for base in reversed(parent_factories): + # Import parent PostGenerationDeclaration + postgen_declarations.update_with_public( + getattr(base, CLASS_ATTRIBUTE_POSTGEN_DECLARATIONS, {})) # Import all 'public' attributes (avoid those starting with _) declarations.update_with_public(getattr(base, CLASS_ATTRIBUTE_DECLARATIONS, {})) - # Import attributes from the class definition, storing protected/private - # attributes in 'non_factory_attrs'. - non_factory_attrs = declarations.update_with_public(attrs) + # Import attributes from the class definition + non_postgen_attrs = postgen_declarations.update_with_public(attrs) + # Store protected/private attributes in 'non_factory_attrs'. + non_factory_attrs = declarations.update_with_public(non_postgen_attrs) # Store the DeclarationDict in the attributes of the newly created class non_factory_attrs[CLASS_ATTRIBUTE_DECLARATIONS] = declarations + non_factory_attrs[CLASS_ATTRIBUTE_POSTGEN_DECLARATIONS] = postgen_declarations # Add extra args if provided. if extra_attrs: @@ -521,20 +528,37 @@ class Factory(BaseFactory): return cls.get_building_function()(getattr(cls, CLASS_ATTRIBUTE_ASSOCIATED_CLASS), **kwargs) @classmethod - def _build(cls, **kwargs): - return cls._prepare(create=False, **kwargs) + def _generate(cls, create, attrs): + """generate the object. - @classmethod - def _create(cls, **kwargs): - return cls._prepare(create=True, **kwargs) + Args: + create (bool): whether to 'build' or 'create' the object + attrs (dict): attributes to use for generating the object + """ + # Extract declarations used for post-generation + postgen_declarations = getattr(cls, CLASS_ATTRIBUTE_POSTGEN_DECLARATIONS) + postgen_attributes = {} + for name, decl in sorted(postgen_declarations.items()): + postgen_attributes[name] = decl.extract(name, attrs) + + # Generate the object + obj = cls._prepare(create, **attrs) + + # Handle post-generation attributes + for name, decl in sorted(postgen_declarations.items()): + extracted, extracted_kwargs = postgen_attributes[name] + decl.call(obj, create, extracted, **extracted_kwargs) + return obj @classmethod def build(cls, **kwargs): - return cls._build(**cls.attributes(create=False, extra=kwargs)) + attrs = cls.attributes(create=False, extra=kwargs) + return cls._generate(False, attrs) @classmethod def create(cls, **kwargs): - return cls._create(**cls.attributes(create=True, extra=kwargs)) + attrs = cls.attributes(create=True, extra=kwargs) + return cls._generate(True, attrs) class DjangoModelFactory(Factory): diff --git a/factory/declarations.py b/factory/declarations.py index d9d560a..ddc6c78 100644 --- a/factory/declarations.py +++ b/factory/declarations.py @@ -23,6 +23,8 @@ import itertools +from factory import utils + class OrderedDeclaration(object): """A factory declaration. @@ -275,12 +277,12 @@ class PostGenerationDeclaration(object): (object, dict): a tuple containing the attribute at 'name' (if provided) and a dict of extracted attributes """ - extracted = attrs.get(name) + extracted = attrs.pop(name, None) if self.extract_prefix: extract_prefix = self.extract_prefix else: extract_prefix = name - kwargs = utils.extract_dict(extract_prefix) + kwargs = utils.extract_dict(extract_prefix, attrs) return extracted, kwargs def call(self, obj, create, extracted=None, **kwargs): -- cgit v1.2.3 From f8a93d6599b71123789541adf9658cd095402f0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sun, 15 Apr 2012 09:45:11 +0200 Subject: Expose and test factory.RelatedFactory. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Raphaël Barrois --- factory/__init__.py | 1 + 1 file changed, 1 insertion(+) (limited to 'factory') diff --git a/factory/__init__.py b/factory/__init__.py index 1bf8968..179a633 100644 --- a/factory/__init__.py +++ b/factory/__init__.py @@ -60,6 +60,7 @@ from declarations import ( SelfAttribute, ContainerAttribute, SubFactory, + RelatedFactory, lazy_attribute, iterator, -- cgit v1.2.3 From f87f03b583c22e810dced73ed277910d9bc8232a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sun, 15 Apr 2012 10:46:08 +0200 Subject: Rename post_declaration to post_generation. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Raphaël Barrois --- factory/__init__.py | 3 ++- factory/declarations.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) (limited to 'factory') diff --git a/factory/__init__.py b/factory/__init__.py index 179a633..73425aa 100644 --- a/factory/__init__.py +++ b/factory/__init__.py @@ -60,6 +60,7 @@ from declarations import ( SelfAttribute, ContainerAttribute, SubFactory, + PostGeneration, RelatedFactory, lazy_attribute, @@ -68,6 +69,6 @@ from declarations import ( sequence, lazy_attribute_sequence, container_attribute, - post_declaration, + post_generation, ) diff --git a/factory/declarations.py b/factory/declarations.py index ddc6c78..4d6e767 100644 --- a/factory/declarations.py +++ b/factory/declarations.py @@ -309,7 +309,7 @@ class PostGeneration(PostGenerationDeclaration): self.function(obj, create, extracted, **kwargs) -def post_declaration(extract_prefix=None): +def post_generation(extract_prefix=None): def decorator(fun): return PostGeneration(fun, extract_prefix=extract_prefix) return decorator -- cgit v1.2.3 From fbd66ede5617a40f73dfb3f518c9887d48ab401e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Wed, 2 May 2012 10:31:31 +0200 Subject: Typo in PostGenerationMethodCall.__init__ (Closes #14). MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Raphaël Barrois --- factory/declarations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'factory') diff --git a/factory/declarations.py b/factory/declarations.py index 4d6e767..828d8a7 100644 --- a/factory/declarations.py +++ b/factory/declarations.py @@ -353,7 +353,7 @@ class PostGenerationMethodCall(PostGenerationDeclaration): password = factory.PostGenerationMethodCall('set_password', password='') """ def __init__(self, method_name, extract_prefix=None, *args, **kwargs): - super(RelatedFactory, self).__init__(extract_prefix) + super(PostGenerationMethodCall, self).__init__(extract_prefix) self.method_name = method_name self.method_args = args self.method_kwargs = kwargs -- cgit v1.2.3 From d96c651f51b25988235ff79b50c7f9355fb16dd7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Fri, 4 May 2012 01:17:59 +0200 Subject: Only absorb dependant arguments for SubFactory fields (Closes #15). MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Raphaël Barrois --- factory/containers.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) (limited to 'factory') diff --git a/factory/containers.py b/factory/containers.py index 9f480cc..946fbd3 100644 --- a/factory/containers.py +++ b/factory/containers.py @@ -242,7 +242,13 @@ class AttributeBuilder(object): self.factory = factory self._containers = extra.pop('__containers', None) self._attrs = factory.declarations(extra) - self._subfields = utils.multi_extract_dict(self._attrs.keys(), self._attrs) + + attrs_with_subfields = [k for k, v in self._attrs.items() if self.has_subfields(v)] + + self._subfields = utils.multi_extract_dict(attrs_with_subfields, self._attrs) + + def has_subfields(self, value): + return isinstance(value, declarations.SubFactory) def build(self, create): """Build a dictionary of attributes. -- cgit v1.2.3 From 18393e00d21276ba7f8b6d83b51510470781ec69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Fri, 1 Jun 2012 13:35:46 +0200 Subject: Fix sequence count for SubFactory (Closes #16). MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Raphaël Barrois --- factory/declarations.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) (limited to 'factory') diff --git a/factory/declarations.py b/factory/declarations.py index 828d8a7..6b53c88 100644 --- a/factory/declarations.py +++ b/factory/declarations.py @@ -244,12 +244,10 @@ class SubFactory(OrderedDeclaration): defaults.update(extra) defaults['__containers'] = containers - attrs = self.factory.attributes(create, defaults) - if create: - return self.factory.create(**attrs) + return self.factory.create(**defaults) else: - return self.factory.build(**attrs) + return self.factory.build(**defaults) class PostGenerationDeclaration(object): -- cgit v1.2.3 From c3069876b43cc30b24df44ef1b5ada1a1dc0358b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Tue, 19 Jun 2012 12:43:22 +0200 Subject: Version bump to 1.1.4 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Raphaël Barrois --- factory/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'factory') diff --git a/factory/__init__.py b/factory/__init__.py index 73425aa..f806e6f 100644 --- a/factory/__init__.py +++ b/factory/__init__.py @@ -20,7 +20,7 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. -__version__ = '1.1.2' # Remember to change in setup.py as well! +__version__ = '1.1.4' # Remember to change in setup.py as well! __author__ = 'Raphaël Barrois ' from base import ( -- cgit v1.2.3 From cc1f59b3fe772c0e4b18f620192233d56e94a4e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Mon, 9 Jul 2012 20:27:52 +0200 Subject: Fix PostGenerationDeclaration.extract. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Raphaël Barrois --- factory/declarations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'factory') diff --git a/factory/declarations.py b/factory/declarations.py index 6b53c88..83c32ab 100644 --- a/factory/declarations.py +++ b/factory/declarations.py @@ -275,11 +275,11 @@ class PostGenerationDeclaration(object): (object, dict): a tuple containing the attribute at 'name' (if provided) and a dict of extracted attributes """ - extracted = attrs.pop(name, None) if self.extract_prefix: extract_prefix = self.extract_prefix else: extract_prefix = name + extracted = attrs.pop(extract_prefix, None) kwargs = utils.extract_dict(extract_prefix, attrs) return extracted, kwargs -- cgit v1.2.3 From 163efcdb67c3328e739564c53e47b74ff421999f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Mon, 9 Jul 2012 20:28:24 +0200 Subject: Version bump MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Raphaël Barrois --- factory/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'factory') diff --git a/factory/__init__.py b/factory/__init__.py index f806e6f..3753461 100644 --- a/factory/__init__.py +++ b/factory/__init__.py @@ -20,7 +20,7 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. -__version__ = '1.1.4' # Remember to change in setup.py as well! +__version__ = '1.1.5' # Remember to change in setup.py as well! __author__ = 'Raphaël Barrois ' from base import ( -- cgit v1.2.3 From 65db1ac3c0e90bbe541caa1902314aa097f1997a Mon Sep 17 00:00:00 2001 From: Marc Abramowitz Date: Tue, 10 Jul 2012 09:29:18 -0700 Subject: Switch `sorted` argument from `cmp` to `key`. --- factory/utils.py | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) (limited to 'factory') diff --git a/factory/utils.py b/factory/utils.py index 2fcd7ff..ce72a9a 100644 --- a/factory/utils.py +++ b/factory/utils.py @@ -57,16 +57,6 @@ def extract_dict(prefix, kwargs, pop=True, exclude=()): return extracted -def declength_compare(a, b): - """Compare objects, choosing longest first.""" - if len(a) > len(b): - return -1 - elif len(a) < len(b): - return 1 - else: - return cmp(a, b) - - def multi_extract_dict(prefixes, kwargs, pop=True, exclude=()): """Extracts all values from a given list of prefixes. @@ -78,7 +68,7 @@ def multi_extract_dict(prefixes, kwargs, pop=True, exclude=()): """ results = {} exclude = list(exclude) - for prefix in sorted(prefixes, cmp=declength_compare): + for prefix in sorted(prefixes, key=lambda x: -len(x)): extracted = extract_dict(prefix, kwargs, pop=pop, exclude=exclude) results[prefix] = extracted exclude.extend( -- cgit v1.2.3 From f0d7e336f73a188405995b41aa30caaa78a01faa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Tue, 10 Jul 2012 21:22:37 +0200 Subject: utils.multi_extract_dict: improve docstring. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Raphaël Barrois --- factory/utils.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) (limited to 'factory') diff --git a/factory/utils.py b/factory/utils.py index ce72a9a..c592da4 100644 --- a/factory/utils.py +++ b/factory/utils.py @@ -60,7 +60,13 @@ def extract_dict(prefix, kwargs, pop=True, exclude=()): def multi_extract_dict(prefixes, kwargs, pop=True, exclude=()): """Extracts all values from a given list of prefixes. - Arguments have the same meaning as for extract_dict. + Extraction will start with longer prefixes. + + Args: + prefixes (str list): the prefixes to use for lookups + kwargs (dict): the dict from which values should be extracted + pop (bool): whether to use pop (True) or get (False) + exclude (iterable): list of prefixed keys that shouldn't be extracted Returns: dict(str => dict): a dict mapping each prefix to the dict of extracted -- cgit v1.2.3 From a31e87f19b7c193b980d0f54971c12a60e8c7263 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Thu, 9 Aug 2012 02:16:42 +0200 Subject: Introduce 'CircularSubFactory. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Raphaël Barrois --- factory/__init__.py | 1 + factory/declarations.py | 105 +++++++++++++++++++++++++++++++++++++++--------- factory/utils.py | 12 ++++++ 3 files changed, 100 insertions(+), 18 deletions(-) (limited to 'factory') diff --git a/factory/__init__.py b/factory/__init__.py index 3753461..789d88e 100644 --- a/factory/__init__.py +++ b/factory/__init__.py @@ -60,6 +60,7 @@ from declarations import ( SelfAttribute, ContainerAttribute, SubFactory, + CircularSubFactory, PostGeneration, RelatedFactory, diff --git a/factory/declarations.py b/factory/declarations.py index 83c32ab..5e45255 100644 --- a/factory/declarations.py +++ b/factory/declarations.py @@ -208,46 +208,115 @@ class ContainerAttribute(OrderedDeclaration): return self.function(obj, containers) -class SubFactory(OrderedDeclaration): - """Base class for attributes based upon a sub-factory. +class ParameteredAttribute(OrderedDeclaration): + """Base class for attributes expecting parameters. Attributes: - defaults (dict): Overrides to the defaults defined in the wrapped - factory - factory (base.Factory): the wrapped factory + defaults (dict): Default values for the paramters. + May be overridden by call-time parameters. + + Class attributes: + CONTAINERS_FIELD (str): name of the field, if any, where container + information (e.g for SubFactory) should be stored. If empty, + containers data isn't merged into generate() parameters. """ - def __init__(self, factory, **kwargs): - super(SubFactory, self).__init__() + CONTAINERS_FIELD = '__containers' + + def __init__(self, **kwargs): + super(ParameteredAttribute, self).__init__() self.defaults = kwargs - self.factory = factory def evaluate(self, create, extra, containers): """Evaluate the current definition and fill its attributes. Uses attributes definition in the following order: - - attributes defined in the wrapped factory class - - values defined when defining the SubFactory - - additional values defined in attributes + - values defined when defining the ParameteredAttribute + - additional values defined when instantiating the containing factory Args: - create (bool): whether the subfactory should call 'build' or - 'create' + create (bool): whether the parent factory is being 'built' or + 'created' extra (containers.DeclarationDict): extra values that should - override the wrapped factory's defaults + override the defaults containers (list of LazyStub): List of LazyStub for the chain of factories being evaluated, the calling stub being first. """ - defaults = dict(self.defaults) if extra: defaults.update(extra) - defaults['__containers'] = containers + if self.CONTAINERS_FIELD: + defaults[self.CONTAINERS_FIELD] = containers + + return self.generate(create, defaults) + + def generate(self, create, params): + """Actually generate the related attribute. + + Args: + create (bool): whether the calling factory was in 'create' or + 'build' mode + params (dict): parameters inherited from init and evaluation-time + overrides. + + Returns: + Computed value for the current declaration. + """ + raise NotImplementedError() + + +class SubFactory(ParameteredAttribute): + """Base class for attributes based upon a sub-factory. + Attributes: + defaults (dict): Overrides to the defaults defined in the wrapped + factory + factory (base.Factory): the wrapped factory + """ + + def __init__(self, factory, **kwargs): + super(SubFactory, self).__init__(**kwargs) + self.factory = factory + + def get_factory(self): + """Retrieve the wrapped factory.Factory subclass.""" + return self.factory + + def generate(self, create, params): + """Evaluate the current definition and fill its attributes. + + Args: + create (bool): whether the subfactory should call 'build' or + 'create' + params (containers.DeclarationDict): extra values that should + override the wrapped factory's defaults + """ + subfactory = self.get_factory() if create: - return self.factory.create(**defaults) + return subfactory.create(**params) else: - return self.factory.build(**defaults) + return subfactory.build(**params) + + +class CircularSubFactory(SubFactory): + """Use to solve circular dependencies issues.""" + def __init__(self, module_name, factory_name, **kwargs): + super(CircularSubFactory, self).__init__(None, **kwargs) + self.module_name = module_name + self.factory_name = factory_name + + def get_factory(self): + """Retrieve the factory.Factory subclass. + + Its value is cached in the 'factory' attribute, and retrieved through + the factory_getter callable. + """ + if self.factory is None: + factory_class = utils.import_object( + self.module_name, self.factory_name) + + self.factory = factory_class + return self.factory class PostGenerationDeclaration(object): diff --git a/factory/utils.py b/factory/utils.py index c592da4..e7cdf5f 100644 --- a/factory/utils.py +++ b/factory/utils.py @@ -81,3 +81,15 @@ def multi_extract_dict(prefixes, kwargs, pop=True, exclude=()): ['%s%s%s' % (prefix, ATTR_SPLITTER, key) for key in extracted]) return results + + +def import_object(module_name, attribute_name): + """Import an object from its absolute path. + + Example: + >>> import_object('datetime', 'datetime') + + """ + module = __import__(module_name, {}, {}, [attribute_name], 0) + return getattr(module, attribute_name) + -- cgit v1.2.3 From 25d249ac3698f083d483ef3fce83b921017a2668 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Thu, 9 Aug 2012 02:20:00 +0200 Subject: Version bump. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Raphaël Barrois --- factory/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'factory') diff --git a/factory/__init__.py b/factory/__init__.py index 789d88e..950a64d 100644 --- a/factory/__init__.py +++ b/factory/__init__.py @@ -20,7 +20,7 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. -__version__ = '1.1.5' # Remember to change in setup.py as well! +__version__ = '1.2.0' __author__ = 'Raphaël Barrois ' from base import ( -- cgit v1.2.3 From df0b1124cbb9f244dc40f435410ec16462a8fc9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Thu, 16 Aug 2012 19:01:07 +0200 Subject: Mark automatic associated class discovery as deprecated. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Also improve warning reporting. Signed-off-by: Raphaël Barrois --- factory/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'factory') diff --git a/factory/base.py b/factory/base.py index c1dbd98..8ff6151 100644 --- a/factory/base.py +++ b/factory/base.py @@ -182,7 +182,7 @@ class FactoryMetaClass(BaseFactoryMetaClass): FACTORY_CLASS_DECLARATION, associated_name, class_name, - ), PendingDeprecationWarning) + ), DeprecationWarning, 3) return getattr(factory_module, associated_name) -- cgit v1.2.3 From 6efa57cf38f945c55214a94e0e7c12cc7eff474f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Thu, 16 Aug 2012 20:28:21 +0200 Subject: Refactor building_function/creation_function handling. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rely on inheritance instead of handwritten set_creation_function and such. Signed-off-by: Raphaël Barrois --- factory/base.py | 65 ++++++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 60 insertions(+), 5 deletions(-) (limited to 'factory') diff --git a/factory/base.py b/factory/base.py index 8ff6151..159a50c 100644 --- a/factory/base.py +++ b/factory/base.py @@ -316,6 +316,37 @@ class BaseFactory(object): """ return getattr(cls, CLASS_ATTRIBUTE_DECLARATIONS).copy(extra_defs) + @classmethod + def _build(cls, target_class, *args, **kwargs): + """Actually build an instance of the target_class. + + Customization point, will be called once the full set of args and kwargs + has been computed. + + Args: + target_class (type): the class for which an instance should be + built + args (tuple): arguments to use when building the class + kwargs (dict): keyword arguments to use when building the class + """ + + return target_class(*args, **kwargs) + + @classmethod + def _create(cls, target_class, *args, **kwargs): + """Actually create an instance of the target_class. + + Customization point, will be called once the full set of args and kwargs + has been computed. + + Args: + target_class (type): the class for which an instance should be + created + args (tuple): arguments to use when creating the class + kwargs (dict): keyword arguments to use when creating the class + """ + return target_class(*args, **kwargs) + @classmethod def build(cls, **kwargs): """Build an instance of the associated class, with overriden attrs.""" @@ -462,7 +493,7 @@ class Factory(BaseFactory): # Customizing 'create' strategy, using a tuple to keep the creation function # from turning it into an instance method. - _creation_function = (DJANGO_CREATION,) + _creation_function = (None,) @classmethod def set_creation_function(cls, creation_function): @@ -485,11 +516,22 @@ class Factory(BaseFactory): an instance will be created, and keyword arguments for the value of the fields of the instance. """ - return cls._creation_function[0] + creation_function = cls._creation_function[0] + if creation_function: + return creation_function + elif cls._create.__func__ == Factory._create.__func__: + # Backwards compatibility. + # Default creation_function and default _create() behavior. + # The best "Vanilla" _create detection algorithm I found is relying + # on actual method implementation (otherwise, make_factory isn't + # detected as 'default'). + return DJANGO_CREATION + else: + return creation_function # Customizing 'build' strategy, using a tuple to keep the creation function # from turning it into an instance method. - _building_function = (NAIVE_BUILD,) + _building_function = (None,) @classmethod def set_building_function(cls, building_function): @@ -522,10 +564,19 @@ class Factory(BaseFactory): create: bool, whether to create or to build the object **kwargs: arguments to pass to the creation function """ + target_class = getattr(cls, CLASS_ATTRIBUTE_ASSOCIATED_CLASS) if create: - return cls.get_creation_function()(getattr(cls, CLASS_ATTRIBUTE_ASSOCIATED_CLASS), **kwargs) + # Backwards compatibility + creation_function = cls.get_creation_function() + if creation_function: + return creation_function(target_class, **kwargs) + return cls._create(target_class, **kwargs) else: - return cls.get_building_function()(getattr(cls, CLASS_ATTRIBUTE_ASSOCIATED_CLASS), **kwargs) + # Backwards compatibility + building_function = cls.get_building_function() + if building_function: + return building_function(target_class, **kwargs) + return cls._build(target_class, **kwargs) @classmethod def _generate(cls, create, attrs): @@ -581,6 +632,10 @@ class DjangoModelFactory(Factory): except IndexError: return 1 + def _create(cls, target_class, *args, **kwargs): + """Create an instance of the model, and save it to the database.""" + return target_class._default_manager.create(*args, **kwargs) + def make_factory(klass, **kwargs): """Create a new, simple factory for the given class.""" -- cgit v1.2.3 From ebbf350c8594cf5c51ea60d3badbbabbb5a6de68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Thu, 16 Aug 2012 20:28:48 +0200 Subject: Add DeprecationWarnings on set_XXX_function. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Raphaël Barrois --- factory/base.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) (limited to 'factory') diff --git a/factory/base.py b/factory/base.py index 159a50c..93aacde 100644 --- a/factory/base.py +++ b/factory/base.py @@ -32,7 +32,12 @@ CREATE_STRATEGY = 'create' STUB_STRATEGY = 'stub' # Creation functions. Use Factory.set_creation_function() to set a creation function appropriate for your ORM. -DJANGO_CREATION = lambda class_to_create, **kwargs: class_to_create.objects.create(**kwargs) +def DJANGO_CREATION(class_to_create, **kwargs): + warnings.warn( + "Factories defaulting to Django's Foo.objects.create() is deprecated, " + "and will be removed in the future. Please inherit from " + "factory.DjangoModelFactory instead.", PendingDeprecationWarning, 6) + return class_to_create.objects.create(**kwargs) # Building functions. Use Factory.set_building_function() to set a building functions appropriate for your ORM. NAIVE_BUILD = lambda class_to_build, **kwargs: class_to_build(**kwargs) @@ -505,6 +510,10 @@ class Factory(BaseFactory): which an instance will be created. The value of the various fields are passed as keyword arguments. """ + warnings.warn( + "Use of factory.set_creation_function is deprecated, and will be " + "removed in the future. Please override Factory._create() instead.", + PendingDeprecationWarning, 2) cls._creation_function = (creation_function,) @classmethod @@ -543,6 +552,10 @@ class Factory(BaseFactory): which an instance will be built. The value of the various fields are passed as keyword arguments. """ + warnings.warn( + "Use of factory.set_building_function is deprecated, and will be " + "removed in the future. Please override Factory._build() instead.", + PendingDeprecationWarning, 2) cls._building_function = (building_function,) @classmethod -- cgit v1.2.3 From b36bcc41725680fe06e63ff0af7ee5798fceee34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Thu, 16 Aug 2012 23:59:29 +0200 Subject: Improve test coverage. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Raphaël Barrois --- factory/__init__.py | 1 + factory/containers.py | 2 +- factory/declarations.py | 4 ++-- 3 files changed, 4 insertions(+), 3 deletions(-) (limited to 'factory') diff --git a/factory/__init__.py b/factory/__init__.py index 950a64d..d2267f0 100644 --- a/factory/__init__.py +++ b/factory/__init__.py @@ -62,6 +62,7 @@ from declarations import ( SubFactory, CircularSubFactory, PostGeneration, + PostGenerationMethodCall, RelatedFactory, lazy_attribute, diff --git a/factory/containers.py b/factory/containers.py index 946fbd3..6834f60 100644 --- a/factory/containers.py +++ b/factory/containers.py @@ -167,7 +167,7 @@ class PostGenerationDeclarationDict(DeclarationDict): class LazyValue(object): """Some kind of "lazy evaluating" object.""" - def evaluate(self, obj, containers=()): + def evaluate(self, obj, containers=()): # pragma: no cover """Compute the value, using the given object.""" raise NotImplementedError("This is an abstract method.") diff --git a/factory/declarations.py b/factory/declarations.py index 5e45255..77000f2 100644 --- a/factory/declarations.py +++ b/factory/declarations.py @@ -250,7 +250,7 @@ class ParameteredAttribute(OrderedDeclaration): return self.generate(create, defaults) - def generate(self, create, params): + def generate(self, create, params): # pragma: no cover """Actually generate the related attribute. Args: @@ -352,7 +352,7 @@ class PostGenerationDeclaration(object): kwargs = utils.extract_dict(extract_prefix, attrs) return extracted, kwargs - def call(self, obj, create, extracted=None, **kwargs): + def call(self, obj, create, extracted=None, **kwargs): # pragma: no cover """Call this hook; no return value is expected. Args: -- cgit v1.2.3 From 2611973bb62d38ff23471fdc115cdd09e351be50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Fri, 17 Aug 2012 01:22:47 +0200 Subject: Add support for passing non-kwarg parameters to factories. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Raphaël Barrois --- factory/base.py | 16 +++++++++++----- factory/containers.py | 2 +- 2 files changed, 12 insertions(+), 6 deletions(-) (limited to 'factory') diff --git a/factory/base.py b/factory/base.py index 93aacde..a3d91b0 100644 --- a/factory/base.py +++ b/factory/base.py @@ -266,6 +266,9 @@ class BaseFactory(object): # class. _base_factory = None + # List of arguments that should be passed as *args instead of **kwargs + FACTORY_ARG_PARAMETERS = () + @classmethod def _setup_next_sequence(cls): """Set up an initial sequence value for Sequence attributes. @@ -334,7 +337,6 @@ class BaseFactory(object): args (tuple): arguments to use when building the class kwargs (dict): keyword arguments to use when building the class """ - return target_class(*args, **kwargs) @classmethod @@ -578,18 +580,22 @@ class Factory(BaseFactory): **kwargs: arguments to pass to the creation function """ target_class = getattr(cls, CLASS_ATTRIBUTE_ASSOCIATED_CLASS) + + # Extract *args from **kwargs + args = tuple(kwargs.pop(key) for key in cls.FACTORY_ARG_PARAMETERS) + if create: # Backwards compatibility creation_function = cls.get_creation_function() if creation_function: - return creation_function(target_class, **kwargs) - return cls._create(target_class, **kwargs) + return creation_function(target_class, *args, **kwargs) + return cls._create(target_class, *args, **kwargs) else: # Backwards compatibility building_function = cls.get_building_function() if building_function: - return building_function(target_class, **kwargs) - return cls._build(target_class, **kwargs) + return building_function(target_class, *args, **kwargs) + return cls._build(target_class, *args, **kwargs) @classmethod def _generate(cls, create, attrs): diff --git a/factory/containers.py b/factory/containers.py index 6834f60..d50cb71 100644 --- a/factory/containers.py +++ b/factory/containers.py @@ -124,7 +124,7 @@ class DeclarationDict(dict): return False elif isinstance(value, declarations.OrderedDeclaration): return True - return (not name.startswith("_")) + return (not name.startswith("_") and not name.startswith("FACTORY_")) def update_with_public(self, d): """Updates the DeclarationDict from a class definition dict. -- cgit v1.2.3 From 9836e013b4494b5e320c81ec4e3f766522639be2 Mon Sep 17 00:00:00 2001 From: Thomas Goirand Date: Sun, 14 Oct 2012 13:19:59 +0000 Subject: Back to upstream version 1.1.5 --- factory/__init__.py | 4 +- factory/base.py | 88 ++++----------------------------------- factory/containers.py | 4 +- factory/declarations.py | 107 +++++++++--------------------------------------- factory/utils.py | 32 ++++++--------- 5 files changed, 41 insertions(+), 194 deletions(-) (limited to 'factory') diff --git a/factory/__init__.py b/factory/__init__.py index d2267f0..3753461 100644 --- a/factory/__init__.py +++ b/factory/__init__.py @@ -20,7 +20,7 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. -__version__ = '1.2.0' +__version__ = '1.1.5' # Remember to change in setup.py as well! __author__ = 'Raphaël Barrois ' from base import ( @@ -60,9 +60,7 @@ from declarations import ( SelfAttribute, ContainerAttribute, SubFactory, - CircularSubFactory, PostGeneration, - PostGenerationMethodCall, RelatedFactory, lazy_attribute, diff --git a/factory/base.py b/factory/base.py index a3d91b0..c1dbd98 100644 --- a/factory/base.py +++ b/factory/base.py @@ -32,12 +32,7 @@ CREATE_STRATEGY = 'create' STUB_STRATEGY = 'stub' # Creation functions. Use Factory.set_creation_function() to set a creation function appropriate for your ORM. -def DJANGO_CREATION(class_to_create, **kwargs): - warnings.warn( - "Factories defaulting to Django's Foo.objects.create() is deprecated, " - "and will be removed in the future. Please inherit from " - "factory.DjangoModelFactory instead.", PendingDeprecationWarning, 6) - return class_to_create.objects.create(**kwargs) +DJANGO_CREATION = lambda class_to_create, **kwargs: class_to_create.objects.create(**kwargs) # Building functions. Use Factory.set_building_function() to set a building functions appropriate for your ORM. NAIVE_BUILD = lambda class_to_build, **kwargs: class_to_build(**kwargs) @@ -187,7 +182,7 @@ class FactoryMetaClass(BaseFactoryMetaClass): FACTORY_CLASS_DECLARATION, associated_name, class_name, - ), DeprecationWarning, 3) + ), PendingDeprecationWarning) return getattr(factory_module, associated_name) @@ -266,9 +261,6 @@ class BaseFactory(object): # class. _base_factory = None - # List of arguments that should be passed as *args instead of **kwargs - FACTORY_ARG_PARAMETERS = () - @classmethod def _setup_next_sequence(cls): """Set up an initial sequence value for Sequence attributes. @@ -324,36 +316,6 @@ class BaseFactory(object): """ return getattr(cls, CLASS_ATTRIBUTE_DECLARATIONS).copy(extra_defs) - @classmethod - def _build(cls, target_class, *args, **kwargs): - """Actually build an instance of the target_class. - - Customization point, will be called once the full set of args and kwargs - has been computed. - - Args: - target_class (type): the class for which an instance should be - built - args (tuple): arguments to use when building the class - kwargs (dict): keyword arguments to use when building the class - """ - return target_class(*args, **kwargs) - - @classmethod - def _create(cls, target_class, *args, **kwargs): - """Actually create an instance of the target_class. - - Customization point, will be called once the full set of args and kwargs - has been computed. - - Args: - target_class (type): the class for which an instance should be - created - args (tuple): arguments to use when creating the class - kwargs (dict): keyword arguments to use when creating the class - """ - return target_class(*args, **kwargs) - @classmethod def build(cls, **kwargs): """Build an instance of the associated class, with overriden attrs.""" @@ -500,7 +462,7 @@ class Factory(BaseFactory): # Customizing 'create' strategy, using a tuple to keep the creation function # from turning it into an instance method. - _creation_function = (None,) + _creation_function = (DJANGO_CREATION,) @classmethod def set_creation_function(cls, creation_function): @@ -512,10 +474,6 @@ class Factory(BaseFactory): which an instance will be created. The value of the various fields are passed as keyword arguments. """ - warnings.warn( - "Use of factory.set_creation_function is deprecated, and will be " - "removed in the future. Please override Factory._create() instead.", - PendingDeprecationWarning, 2) cls._creation_function = (creation_function,) @classmethod @@ -527,22 +485,11 @@ class Factory(BaseFactory): an instance will be created, and keyword arguments for the value of the fields of the instance. """ - creation_function = cls._creation_function[0] - if creation_function: - return creation_function - elif cls._create.__func__ == Factory._create.__func__: - # Backwards compatibility. - # Default creation_function and default _create() behavior. - # The best "Vanilla" _create detection algorithm I found is relying - # on actual method implementation (otherwise, make_factory isn't - # detected as 'default'). - return DJANGO_CREATION - else: - return creation_function + return cls._creation_function[0] # Customizing 'build' strategy, using a tuple to keep the creation function # from turning it into an instance method. - _building_function = (None,) + _building_function = (NAIVE_BUILD,) @classmethod def set_building_function(cls, building_function): @@ -554,10 +501,6 @@ class Factory(BaseFactory): which an instance will be built. The value of the various fields are passed as keyword arguments. """ - warnings.warn( - "Use of factory.set_building_function is deprecated, and will be " - "removed in the future. Please override Factory._build() instead.", - PendingDeprecationWarning, 2) cls._building_function = (building_function,) @classmethod @@ -579,23 +522,10 @@ class Factory(BaseFactory): create: bool, whether to create or to build the object **kwargs: arguments to pass to the creation function """ - target_class = getattr(cls, CLASS_ATTRIBUTE_ASSOCIATED_CLASS) - - # Extract *args from **kwargs - args = tuple(kwargs.pop(key) for key in cls.FACTORY_ARG_PARAMETERS) - if create: - # Backwards compatibility - creation_function = cls.get_creation_function() - if creation_function: - return creation_function(target_class, *args, **kwargs) - return cls._create(target_class, *args, **kwargs) + return cls.get_creation_function()(getattr(cls, CLASS_ATTRIBUTE_ASSOCIATED_CLASS), **kwargs) else: - # Backwards compatibility - building_function = cls.get_building_function() - if building_function: - return building_function(target_class, *args, **kwargs) - return cls._build(target_class, *args, **kwargs) + return cls.get_building_function()(getattr(cls, CLASS_ATTRIBUTE_ASSOCIATED_CLASS), **kwargs) @classmethod def _generate(cls, create, attrs): @@ -651,10 +581,6 @@ class DjangoModelFactory(Factory): except IndexError: return 1 - def _create(cls, target_class, *args, **kwargs): - """Create an instance of the model, and save it to the database.""" - return target_class._default_manager.create(*args, **kwargs) - def make_factory(klass, **kwargs): """Create a new, simple factory for the given class.""" diff --git a/factory/containers.py b/factory/containers.py index d50cb71..946fbd3 100644 --- a/factory/containers.py +++ b/factory/containers.py @@ -124,7 +124,7 @@ class DeclarationDict(dict): return False elif isinstance(value, declarations.OrderedDeclaration): return True - return (not name.startswith("_") and not name.startswith("FACTORY_")) + return (not name.startswith("_")) def update_with_public(self, d): """Updates the DeclarationDict from a class definition dict. @@ -167,7 +167,7 @@ class PostGenerationDeclarationDict(DeclarationDict): class LazyValue(object): """Some kind of "lazy evaluating" object.""" - def evaluate(self, obj, containers=()): # pragma: no cover + def evaluate(self, obj, containers=()): """Compute the value, using the given object.""" raise NotImplementedError("This is an abstract method.") diff --git a/factory/declarations.py b/factory/declarations.py index 77000f2..83c32ab 100644 --- a/factory/declarations.py +++ b/factory/declarations.py @@ -208,115 +208,46 @@ class ContainerAttribute(OrderedDeclaration): return self.function(obj, containers) -class ParameteredAttribute(OrderedDeclaration): - """Base class for attributes expecting parameters. +class SubFactory(OrderedDeclaration): + """Base class for attributes based upon a sub-factory. Attributes: - defaults (dict): Default values for the paramters. - May be overridden by call-time parameters. - - Class attributes: - CONTAINERS_FIELD (str): name of the field, if any, where container - information (e.g for SubFactory) should be stored. If empty, - containers data isn't merged into generate() parameters. + defaults (dict): Overrides to the defaults defined in the wrapped + factory + factory (base.Factory): the wrapped factory """ - CONTAINERS_FIELD = '__containers' - - def __init__(self, **kwargs): - super(ParameteredAttribute, self).__init__() + def __init__(self, factory, **kwargs): + super(SubFactory, self).__init__() self.defaults = kwargs + self.factory = factory def evaluate(self, create, extra, containers): """Evaluate the current definition and fill its attributes. Uses attributes definition in the following order: - - values defined when defining the ParameteredAttribute - - additional values defined when instantiating the containing factory + - attributes defined in the wrapped factory class + - values defined when defining the SubFactory + - additional values defined in attributes Args: - create (bool): whether the parent factory is being 'built' or - 'created' + create (bool): whether the subfactory should call 'build' or + 'create' extra (containers.DeclarationDict): extra values that should - override the defaults + override the wrapped factory's defaults containers (list of LazyStub): List of LazyStub for the chain of factories being evaluated, the calling stub being first. """ + defaults = dict(self.defaults) if extra: defaults.update(extra) - if self.CONTAINERS_FIELD: - defaults[self.CONTAINERS_FIELD] = containers + defaults['__containers'] = containers - return self.generate(create, defaults) - - def generate(self, create, params): # pragma: no cover - """Actually generate the related attribute. - - Args: - create (bool): whether the calling factory was in 'create' or - 'build' mode - params (dict): parameters inherited from init and evaluation-time - overrides. - - Returns: - Computed value for the current declaration. - """ - raise NotImplementedError() - - -class SubFactory(ParameteredAttribute): - """Base class for attributes based upon a sub-factory. - - Attributes: - defaults (dict): Overrides to the defaults defined in the wrapped - factory - factory (base.Factory): the wrapped factory - """ - - def __init__(self, factory, **kwargs): - super(SubFactory, self).__init__(**kwargs) - self.factory = factory - - def get_factory(self): - """Retrieve the wrapped factory.Factory subclass.""" - return self.factory - - def generate(self, create, params): - """Evaluate the current definition and fill its attributes. - - Args: - create (bool): whether the subfactory should call 'build' or - 'create' - params (containers.DeclarationDict): extra values that should - override the wrapped factory's defaults - """ - subfactory = self.get_factory() if create: - return subfactory.create(**params) + return self.factory.create(**defaults) else: - return subfactory.build(**params) - - -class CircularSubFactory(SubFactory): - """Use to solve circular dependencies issues.""" - def __init__(self, module_name, factory_name, **kwargs): - super(CircularSubFactory, self).__init__(None, **kwargs) - self.module_name = module_name - self.factory_name = factory_name - - def get_factory(self): - """Retrieve the factory.Factory subclass. - - Its value is cached in the 'factory' attribute, and retrieved through - the factory_getter callable. - """ - if self.factory is None: - factory_class = utils.import_object( - self.module_name, self.factory_name) - - self.factory = factory_class - return self.factory + return self.factory.build(**defaults) class PostGenerationDeclaration(object): @@ -352,7 +283,7 @@ class PostGenerationDeclaration(object): kwargs = utils.extract_dict(extract_prefix, attrs) return extracted, kwargs - def call(self, obj, create, extracted=None, **kwargs): # pragma: no cover + def call(self, obj, create, extracted=None, **kwargs): """Call this hook; no return value is expected. Args: diff --git a/factory/utils.py b/factory/utils.py index e7cdf5f..2fcd7ff 100644 --- a/factory/utils.py +++ b/factory/utils.py @@ -57,16 +57,20 @@ def extract_dict(prefix, kwargs, pop=True, exclude=()): return extracted +def declength_compare(a, b): + """Compare objects, choosing longest first.""" + if len(a) > len(b): + return -1 + elif len(a) < len(b): + return 1 + else: + return cmp(a, b) + + def multi_extract_dict(prefixes, kwargs, pop=True, exclude=()): """Extracts all values from a given list of prefixes. - Extraction will start with longer prefixes. - - Args: - prefixes (str list): the prefixes to use for lookups - kwargs (dict): the dict from which values should be extracted - pop (bool): whether to use pop (True) or get (False) - exclude (iterable): list of prefixed keys that shouldn't be extracted + Arguments have the same meaning as for extract_dict. Returns: dict(str => dict): a dict mapping each prefix to the dict of extracted @@ -74,22 +78,10 @@ def multi_extract_dict(prefixes, kwargs, pop=True, exclude=()): """ results = {} exclude = list(exclude) - for prefix in sorted(prefixes, key=lambda x: -len(x)): + for prefix in sorted(prefixes, cmp=declength_compare): extracted = extract_dict(prefix, kwargs, pop=pop, exclude=exclude) results[prefix] = extracted exclude.extend( ['%s%s%s' % (prefix, ATTR_SPLITTER, key) for key in extracted]) return results - - -def import_object(module_name, attribute_name): - """Import an object from its absolute path. - - Example: - >>> import_object('datetime', 'datetime') - - """ - module = __import__(module_name, {}, {}, [attribute_name], 0) - return getattr(module, attribute_name) - -- cgit v1.2.3 From 9db320ba65d91ff8c09169b359ded4bfff5196db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Fri, 17 Aug 2012 17:00:01 +0200 Subject: Use proper relative/absolute imports. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Raphaël Barrois --- factory/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'factory') diff --git a/factory/__init__.py b/factory/__init__.py index d2267f0..fd37c74 100644 --- a/factory/__init__.py +++ b/factory/__init__.py @@ -23,7 +23,7 @@ __version__ = '1.2.0' __author__ = 'Raphaël Barrois ' -from base import ( +from .base import ( Factory, StubFactory, DjangoModelFactory, @@ -51,7 +51,7 @@ from base import ( MOGO_BUILD, ) -from declarations import ( +from .declarations import ( LazyAttribute, Iterator, InfiniteIterator, -- cgit v1.2.3 From b093cc6a35c884b926e0c2bc5928b330cccd4e03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Fri, 17 Aug 2012 17:02:48 +0200 Subject: [py3] Remove calls to iteritems(). MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Raphaël Barrois --- factory/base.py | 2 +- factory/containers.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) (limited to 'factory') diff --git a/factory/base.py b/factory/base.py index a3d91b0..240170c 100644 --- a/factory/base.py +++ b/factory/base.py @@ -396,7 +396,7 @@ class BaseFactory(object): factory's declarations or in the extra kwargs. """ stub_object = containers.StubObject() - for name, value in cls.attributes(create=False, extra=kwargs).iteritems(): + for name, value in cls.attributes(create=False, extra=kwargs).items(): setattr(stub_object, name, value) return stub_object diff --git a/factory/containers.py b/factory/containers.py index d50cb71..46a647f 100644 --- a/factory/containers.py +++ b/factory/containers.py @@ -136,7 +136,7 @@ class DeclarationDict(dict): Returns a dict containing all remaining elements. """ remaining = {} - for k, v in d.iteritems(): + for k, v in d.items(): if self.is_declaration(k, v): self[k] = v else: @@ -262,7 +262,7 @@ class AttributeBuilder(object): # Parse attribute declarations, wrapping SubFactory and # OrderedDeclaration. wrapped_attrs = {} - for k, v in self._attrs.iteritems(): + for k, v in self._attrs.items(): if isinstance(v, declarations.SubFactory): v = SubFactoryWrapper(v, self._subfields.get(k, {}), create) elif isinstance(v, declarations.OrderedDeclaration): -- cgit v1.2.3 From b152ba79ab355c231b6e5fd852bad546e06208d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Fri, 17 Aug 2012 17:08:33 +0200 Subject: [py3] Rename xrange to range MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Raphaël Barrois --- factory/base.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'factory') diff --git a/factory/base.py b/factory/base.py index 240170c..a5056fd 100644 --- a/factory/base.py +++ b/factory/base.py @@ -369,7 +369,7 @@ class BaseFactory(object): Returns: object list: the built instances """ - return [cls.build(**kwargs) for _ in xrange(size)] + return [cls.build(**kwargs) for _ in range(size)] @classmethod def create(cls, **kwargs): @@ -386,7 +386,7 @@ class BaseFactory(object): Returns: object list: the created instances """ - return [cls.create(**kwargs) for _ in xrange(size)] + return [cls.create(**kwargs) for _ in range(size)] @classmethod def stub(cls, **kwargs): @@ -410,7 +410,7 @@ class BaseFactory(object): Returns: object list: the stubbed instances """ - return [cls.stub(**kwargs) for _ in xrange(size)] + return [cls.stub(**kwargs) for _ in range(size)] @classmethod def generate(cls, strategy, **kwargs): -- cgit v1.2.3 From ac90ac4b3425cc79c164b3dc0bd13901bf814ff7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Fri, 17 Aug 2012 17:11:17 +0200 Subject: [py3] Various python3-compatibility fixes. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Raphaël Barrois --- factory/containers.py | 2 +- factory/declarations.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) (limited to 'factory') diff --git a/factory/containers.py b/factory/containers.py index 46a647f..4ceb07f 100644 --- a/factory/containers.py +++ b/factory/containers.py @@ -62,7 +62,7 @@ class LazyStub(object): def __str__(self): return '' % ( - self.__target_class.__name__, self.__attrs.keys()) + self.__target_class.__name__, list(self.__attrs.keys())) def __fill__(self): """Fill this LazyStub, computing values of all defined attributes. diff --git a/factory/declarations.py b/factory/declarations.py index 77000f2..50a826f 100644 --- a/factory/declarations.py +++ b/factory/declarations.py @@ -129,7 +129,7 @@ class Iterator(OrderedDeclaration): self.iterator = iter(iterator) def evaluate(self, sequence, obj, containers=()): - return self.iterator.next() + return next(self.iterator) class InfiniteIterator(Iterator): -- cgit v1.2.3 From c86d32b892c383fb18b0a5d7cebc7671e4e88ab1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Wed, 14 Nov 2012 23:15:55 +0100 Subject: Mix SelfAttribute with ContainerAttribute. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit With a very simple syntax. Signed-off-by: Raphaël Barrois --- factory/declarations.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) (limited to 'factory') diff --git a/factory/declarations.py b/factory/declarations.py index 50a826f..fe1afa4 100644 --- a/factory/declarations.py +++ b/factory/declarations.py @@ -100,7 +100,11 @@ def deepgetattr(obj, name, default=_UNSPECIFIED): class SelfAttribute(OrderedDeclaration): """Specific OrderedDeclaration copying values from other fields. + If the field name starts with two dots or more, the lookup will be anchored + in the related 'parent'. + Attributes: + depth (int): the number of steps to go up in the containers chain attribute_name (str): the name of the attribute to copy. default (object): the default value to use if the attribute doesn't exist. @@ -108,11 +112,20 @@ class SelfAttribute(OrderedDeclaration): def __init__(self, attribute_name, default=_UNSPECIFIED, *args, **kwargs): super(SelfAttribute, self).__init__(*args, **kwargs) + depth = len(attribute_name) - len(attribute_name.lstrip('.')) + attribute_name = attribute_name[depth:] + + self.depth = depth self.attribute_name = attribute_name self.default = default def evaluate(self, sequence, obj, containers=()): - return deepgetattr(obj, self.attribute_name, self.default) + if self.depth > 1: + # Fetching from a parent + target = containers[self.depth - 2] + else: + target = obj + return deepgetattr(target, self.attribute_name, self.default) class Iterator(OrderedDeclaration): -- cgit v1.2.3 From 14640a61eca84a7624bc1994233529dcacc2417e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Wed, 14 Nov 2012 23:35:03 +0100 Subject: Remove deprecated _*_function class attributes. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Setting them will still work as intended, though. Signed-off-by: Raphaël Barrois --- factory/base.py | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) (limited to 'factory') diff --git a/factory/base.py b/factory/base.py index a5056fd..4b4f5af 100644 --- a/factory/base.py +++ b/factory/base.py @@ -498,10 +498,6 @@ class Factory(BaseFactory): class AssociatedClassError(RuntimeError): pass - # Customizing 'create' strategy, using a tuple to keep the creation function - # from turning it into an instance method. - _creation_function = (None,) - @classmethod def set_creation_function(cls, creation_function): """Set the creation function for this class. @@ -516,6 +512,8 @@ class Factory(BaseFactory): "Use of factory.set_creation_function is deprecated, and will be " "removed in the future. Please override Factory._create() instead.", PendingDeprecationWarning, 2) + # Customizing 'create' strategy, using a tuple to keep the creation function + # from turning it into an instance method. cls._creation_function = (creation_function,) @classmethod @@ -527,9 +525,9 @@ class Factory(BaseFactory): an instance will be created, and keyword arguments for the value of the fields of the instance. """ - creation_function = cls._creation_function[0] - if creation_function: - return creation_function + creation_function = getattr(cls, '_creation_function', ()) + if creation_function and creation_function[0]: + return creation_function[0] elif cls._create.__func__ == Factory._create.__func__: # Backwards compatibility. # Default creation_function and default _create() behavior. @@ -537,12 +535,6 @@ class Factory(BaseFactory): # on actual method implementation (otherwise, make_factory isn't # detected as 'default'). return DJANGO_CREATION - else: - return creation_function - - # Customizing 'build' strategy, using a tuple to keep the creation function - # from turning it into an instance method. - _building_function = (None,) @classmethod def set_building_function(cls, building_function): @@ -558,6 +550,8 @@ class Factory(BaseFactory): "Use of factory.set_building_function is deprecated, and will be " "removed in the future. Please override Factory._build() instead.", PendingDeprecationWarning, 2) + # Customizing 'build' strategy, using a tuple to keep the creation function + # from turning it into an instance method. cls._building_function = (building_function,) @classmethod @@ -569,7 +563,9 @@ class Factory(BaseFactory): an instance will be created, and keyword arguments for the value of the fields of the instance. """ - return cls._building_function[0] + building_function = getattr(cls, '_building_function', ()) + if building_function and building_function[0]: + return building_function[0] @classmethod def _prepare(cls, create, **kwargs): -- cgit v1.2.3 From a19f64cbfadc0e36b2ff9812980e23955276632c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Wed, 14 Nov 2012 23:42:30 +0100 Subject: Update docstrings. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Raphaël Barrois --- factory/base.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) (limited to 'factory') diff --git a/factory/base.py b/factory/base.py index 4b4f5af..20a3a6b 100644 --- a/factory/base.py +++ b/factory/base.py @@ -31,7 +31,8 @@ BUILD_STRATEGY = 'build' CREATE_STRATEGY = 'create' STUB_STRATEGY = 'stub' -# Creation functions. Use Factory.set_creation_function() to set a creation function appropriate for your ORM. +# Creation functions. Deprecated. +# Override Factory._create instead. def DJANGO_CREATION(class_to_create, **kwargs): warnings.warn( "Factories defaulting to Django's Foo.objects.create() is deprecated, " @@ -39,7 +40,8 @@ def DJANGO_CREATION(class_to_create, **kwargs): "factory.DjangoModelFactory instead.", PendingDeprecationWarning, 6) return class_to_create.objects.create(**kwargs) -# Building functions. Use Factory.set_building_function() to set a building functions appropriate for your ORM. +# Building functions. Deprecated. +# Override Factory._build instead. NAIVE_BUILD = lambda class_to_build, **kwargs: class_to_build(**kwargs) MOGO_BUILD = lambda class_to_build, **kwargs: class_to_build.new(**kwargs) -- cgit v1.2.3 From 5ec4a50edc67073e54218549d6985f934f94b88f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Wed, 14 Nov 2012 23:42:46 +0100 Subject: Add an extension point for kwargs mangling. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Raphaël Barrois --- factory/base.py | 6 ++++++ 1 file changed, 6 insertions(+) (limited to 'factory') diff --git a/factory/base.py b/factory/base.py index 20a3a6b..59f37eb 100644 --- a/factory/base.py +++ b/factory/base.py @@ -569,6 +569,11 @@ class Factory(BaseFactory): if building_function and building_function[0]: return building_function[0] + @classmethod + def _adjust_kwargs(cls, **kwargs): + """Extension point for custom kwargs adjustment.""" + return kwargs + @classmethod def _prepare(cls, create, **kwargs): """Prepare an object for this factory. @@ -578,6 +583,7 @@ class Factory(BaseFactory): **kwargs: arguments to pass to the creation function """ target_class = getattr(cls, CLASS_ATTRIBUTE_ASSOCIATED_CLASS) + kwargs = cls._adjust_kwargs(**kwargs) # Extract *args from **kwargs args = tuple(kwargs.pop(key) for key in cls.FACTORY_ARG_PARAMETERS) -- cgit v1.2.3 From 7fe9c7cc94f6ba69abdf45d09a2dcc8969503514 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Wed, 14 Nov 2012 23:43:03 +0100 Subject: Add MogoFactory. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A Factory subclass, along the lines of DjangoModelFactory. Signed-off-by: Raphaël Barrois --- factory/base.py | 8 ++++++++ 1 file changed, 8 insertions(+) (limited to 'factory') diff --git a/factory/base.py b/factory/base.py index 59f37eb..dde9f38 100644 --- a/factory/base.py +++ b/factory/base.py @@ -660,6 +660,14 @@ class DjangoModelFactory(Factory): return target_class._default_manager.create(*args, **kwargs) +class MogoFactory(Factory): + """Factory for mogo objects.""" + ABSTRACT_FACTORY = True + + def _build(cls, target_class, *args, **kwargs): + return target_class.new(*args, **kwargs) + + def make_factory(klass, **kwargs): """Create a new, simple factory for the given class.""" factory_name = '%sFactory' % klass.__name__ -- cgit v1.2.3 From 048fb1cc935a2ccbc5ca7af81c9390e218a1080b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Wed, 14 Nov 2012 23:51:09 +0100 Subject: Rename ABSTRACT_FACTORY to FACTORY_ABSTRACT. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit And add a deprecation warning too. Signed-off-by: Raphaël Barrois --- factory/base.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) (limited to 'factory') diff --git a/factory/base.py b/factory/base.py index dde9f38..3d4e8c1 100644 --- a/factory/base.py +++ b/factory/base.py @@ -170,7 +170,7 @@ class FactoryMetaClass(BaseFactoryMetaClass): if FACTORY_CLASS_DECLARATION in attrs: return attrs[FACTORY_CLASS_DECLARATION] - # No specific associated calss was given, and one was defined for our + # No specific associated class was given, and one was defined for our # parent, use it. if inherited is not None: return inherited @@ -212,12 +212,21 @@ class FactoryMetaClass(BaseFactoryMetaClass): for construction of an associated class instance at a later time.""" parent_factories = get_factory_bases(bases) - if not parent_factories or attrs.get('ABSTRACT_FACTORY', False): + if not parent_factories or attrs.get('ABSTRACT_FACTORY', False) \ + or attrs.get('FACTORY_ABSTRACT', False): # If this isn't a subclass of Factory, or specifically declared # abstract, don't do anything special. if 'ABSTRACT_FACTORY' in attrs: + warnings.warn( + "The 'ABSTRACT_FACTORY' class attribute has been renamed " + "to 'FACTORY_ABSTRACT' for naming consistency, and will " + "be ignored in the future. Please upgrade class %s." % + class_name, DeprecationWarning, 2) attrs.pop('ABSTRACT_FACTORY') + if 'FACTORY_ABSTRACT' in attrs: + attrs.pop('FACTORY_ABSTRACT') + return super(FactoryMetaClass, cls).__new__(cls, class_name, bases, attrs) base = parent_factories[0] @@ -644,7 +653,7 @@ class DjangoModelFactory(Factory): handle those for non-numerical primary keys. """ - ABSTRACT_FACTORY = True + FACTORY_ABSTRACT = True @classmethod def _setup_next_sequence(cls): @@ -662,7 +671,7 @@ class DjangoModelFactory(Factory): class MogoFactory(Factory): """Factory for mogo objects.""" - ABSTRACT_FACTORY = True + FACTORY_ABSTRACT = True def _build(cls, target_class, *args, **kwargs): return target_class.new(*args, **kwargs) -- cgit v1.2.3 From a5184785b1229e02ec75506db675a2377b32c297 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Wed, 14 Nov 2012 23:53:26 +0100 Subject: Keep FACTORY_FOR around. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit And add a test too. Signed-off-by: Raphaël Barrois --- factory/base.py | 3 --- 1 file changed, 3 deletions(-) (limited to 'factory') diff --git a/factory/base.py b/factory/base.py index 3d4e8c1..2b78e2b 100644 --- a/factory/base.py +++ b/factory/base.py @@ -236,9 +236,6 @@ class FactoryMetaClass(BaseFactoryMetaClass): associated_class = cls._discover_associated_class(class_name, attrs, inherited_associated_class) - # Remove the FACTORY_CLASS_DECLARATION attribute from attrs, if present. - attrs.pop(FACTORY_CLASS_DECLARATION, None) - # If inheriting the factory from a parent, keep a link to it. # This allows to use the sequence counters from the parents. if associated_class == inherited_associated_class: -- cgit v1.2.3 From 63c88fb17db0156606b87f4014b2b6275b261564 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Thu, 15 Nov 2012 02:01:11 +0100 Subject: Update my email; MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Raphaël Barrois --- factory/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'factory') diff --git a/factory/__init__.py b/factory/__init__.py index fd37c74..a2a9f9a 100644 --- a/factory/__init__.py +++ b/factory/__init__.py @@ -21,7 +21,7 @@ # THE SOFTWARE. __version__ = '1.2.0' -__author__ = 'Raphaël Barrois ' +__author__ = 'Raphaël Barrois ' from .base import ( Factory, -- cgit v1.2.3 From b449fbf4d9f7d9b93b1c0f400cf562953b209534 Mon Sep 17 00:00:00 2001 From: Eduard Iskandarov Date: Mon, 19 Nov 2012 13:41:43 +0600 Subject: Fix pk lookup in _setup_next_sequence method. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #31 Signed-off-by: Raphaël Barrois --- factory/base.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'factory') diff --git a/factory/base.py b/factory/base.py index 2b78e2b..8de652d 100644 --- a/factory/base.py +++ b/factory/base.py @@ -654,10 +654,10 @@ class DjangoModelFactory(Factory): @classmethod def _setup_next_sequence(cls): - """Compute the next available ID, based on the 'id' database field.""" + """Compute the next available PK, based on the 'pk' database field.""" try: - return 1 + cls._associated_class._default_manager.values_list('id', flat=True - ).order_by('-id')[0] + return 1 + cls._associated_class._default_manager.values_list('pk', flat=True + ).order_by('-pk')[0] except IndexError: return 1 -- cgit v1.2.3 From 85ded9c9dc0f1c0b57d360b4cf54fe1aba2f8ca7 Mon Sep 17 00:00:00 2001 From: obiwanus Date: Wed, 5 Dec 2012 20:52:51 +0400 Subject: Add classmethod decorator to child factories methods MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #33,#34 Signed-off-by: Raphaël Barrois --- factory/base.py | 2 ++ 1 file changed, 2 insertions(+) (limited to 'factory') diff --git a/factory/base.py b/factory/base.py index 8de652d..3d3d383 100644 --- a/factory/base.py +++ b/factory/base.py @@ -661,6 +661,7 @@ class DjangoModelFactory(Factory): except IndexError: return 1 + @classmethod def _create(cls, target_class, *args, **kwargs): """Create an instance of the model, and save it to the database.""" return target_class._default_manager.create(*args, **kwargs) @@ -670,6 +671,7 @@ class MogoFactory(Factory): """Factory for mogo objects.""" FACTORY_ABSTRACT = True + @classmethod def _build(cls, target_class, *args, **kwargs): return target_class.new(*args, **kwargs) -- cgit v1.2.3 From c6182ccc61d2224275fb4af1f7807f91114e0bee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sun, 9 Dec 2012 01:59:00 +0100 Subject: Fix version numbering. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Somehow, I forgot that I had release 1.2.0 :/ Signed-off-by: Raphaël Barrois --- factory/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'factory') diff --git a/factory/__init__.py b/factory/__init__.py index a2a9f9a..a07ffa1 100644 --- a/factory/__init__.py +++ b/factory/__init__.py @@ -20,7 +20,7 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. -__version__ = '1.2.0' +__version__ = '1.3.0-dev' __author__ = 'Raphaël Barrois ' from .base import ( -- cgit v1.2.3 From 8c3c45e441e589a4ede80bb8532803a382624332 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Wed, 2 Jan 2013 10:48:43 +0100 Subject: Happy New Year! MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Raphaël Barrois --- factory/__init__.py | 2 +- factory/base.py | 2 +- factory/containers.py | 2 +- factory/declarations.py | 2 +- factory/utils.py | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) (limited to 'factory') diff --git a/factory/__init__.py b/factory/__init__.py index a07ffa1..0cf1b82 100644 --- a/factory/__init__.py +++ b/factory/__init__.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # Copyright (c) 2010 Mark Sandstrom -# Copyright (c) 2011 Raphaël Barrois +# Copyright (c) 2011-2013 Raphaël Barrois # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal diff --git a/factory/base.py b/factory/base.py index 3d3d383..c8c7f06 100644 --- a/factory/base.py +++ b/factory/base.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # Copyright (c) 2010 Mark Sandstrom -# Copyright (c) 2011 Raphaël Barrois +# Copyright (c) 2011-2013 Raphaël Barrois # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal diff --git a/factory/containers.py b/factory/containers.py index 4ceb07f..31ee58b 100644 --- a/factory/containers.py +++ b/factory/containers.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # Copyright (c) 2010 Mark Sandstrom -# Copyright (c) 2011 Raphaël Barrois +# Copyright (c) 2011-2013 Raphaël Barrois # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal diff --git a/factory/declarations.py b/factory/declarations.py index fe1afa4..c64a0e5 100644 --- a/factory/declarations.py +++ b/factory/declarations.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # Copyright (c) 2010 Mark Sandstrom -# Copyright (c) 2011 Raphaël Barrois +# Copyright (c) 2011-2013 Raphaël Barrois # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal diff --git a/factory/utils.py b/factory/utils.py index e7cdf5f..90fdfc3 100644 --- a/factory/utils.py +++ b/factory/utils.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # Copyright (c) 2010 Mark Sandstrom -# Copyright (c) 2011 Raphaël Barrois +# Copyright (c) 2011-2013 Raphaël Barrois # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal -- cgit v1.2.3 From ebdfb8cc5bab1e59b593a4ea60e55b9e7af455ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Fri, 1 Mar 2013 01:35:26 +0100 Subject: Improve Iterator and SubFactory declarations. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Iterator now cycles by default * Iterator can be provided with a custom getter * SubFactory accepts a factory import path as well Deprecates: * InfiniteIterator * CircularSubFactory Signed-off-by: Raphaël Barrois --- factory/declarations.py | 65 ++++++++++++++++++++++++++++++++++--------------- 1 file changed, 45 insertions(+), 20 deletions(-) (limited to 'factory') diff --git a/factory/declarations.py b/factory/declarations.py index c64a0e5..d5b7950 100644 --- a/factory/declarations.py +++ b/factory/declarations.py @@ -22,6 +22,7 @@ import itertools +import warnings from factory import utils @@ -135,14 +136,23 @@ class Iterator(OrderedDeclaration): Attributes: iterator (iterable): the iterator whose value should be used. + getter (callable or None): a function to parse returned values """ - def __init__(self, iterator): + def __init__(self, iterator, cycle=True, getter=None): super(Iterator, self).__init__() - self.iterator = iter(iterator) + self.getter = getter + + if cycle: + self.iterator = itertools.cycle(iterator) + else: + self.iterator = iter(iterator) def evaluate(self, sequence, obj, containers=()): - return next(self.iterator) + value = next(self.iterator) + if self.getter is None: + return value + return self.getter(value) class InfiniteIterator(Iterator): @@ -153,7 +163,11 @@ class InfiniteIterator(Iterator): """ def __init__(self, iterator): - return super(InfiniteIterator, self).__init__(itertools.cycle(iterator)) + warnings.warn( + "factory.InfiniteIterator is deprecated, and will be removed in the " + "future. Please use factory.Iterator instead.", + PendingDeprecationWarning, 2) + return super(InfiniteIterator, self).__init__(iterator, cycle=True) class Sequence(OrderedDeclaration): @@ -289,10 +303,24 @@ class SubFactory(ParameteredAttribute): def __init__(self, factory, **kwargs): super(SubFactory, self).__init__(**kwargs) - self.factory = factory + if isinstance(factory, type): + self.factory = factory + self.factory_module = self.factory_name = '' + else: + # Must be a string + if not isinstance(factory, basestring) or '.' not in factory: + raise ValueError( + "The argument of a SubFactory must be either a class " + "or the fully qualified path to a Factory class; got " + "%r instead." % factory) + self.factory = None + self.factory_module, self.factory_name = factory.rsplit('.', 1) def get_factory(self): """Retrieve the wrapped factory.Factory subclass.""" + if self.factory is None: + # Must be a module path + self.factory = utils.import_object(self.factory_module, self.factory_name) return self.factory def generate(self, create, params): @@ -314,22 +342,15 @@ class SubFactory(ParameteredAttribute): class CircularSubFactory(SubFactory): """Use to solve circular dependencies issues.""" def __init__(self, module_name, factory_name, **kwargs): - super(CircularSubFactory, self).__init__(None, **kwargs) - self.module_name = module_name - self.factory_name = factory_name + factory = '%s.%s' % (module_name, factory_name) + warnings.warn( + "factory.CircularSubFactory is deprecated and will be removed in " + "the future. " + "Please replace factory.CircularSubFactory('module', 'symbol') " + "with factory.SubFactory('module.symbol').", + PendingDeprecationWarning, 2) - def get_factory(self): - """Retrieve the factory.Factory subclass. - - Its value is cached in the 'factory' attribute, and retrieved through - the factory_getter callable. - """ - if self.factory is None: - factory_class = utils.import_object( - self.module_name, self.factory_name) - - self.factory = factory_class - return self.factory + super(CircularSubFactory, self).__init__(factory, **kwargs) class PostGenerationDeclaration(object): @@ -456,6 +477,10 @@ def iterator(func): def infinite_iterator(func): """Turn a generator function into an infinite iterator attribute.""" + warnings.warn( + "@factory.infinite_iterator is deprecated and will be removed in the " + "future. Please use @factory.iterator instead.", + PendingDeprecationWarning, 2) return InfiniteIterator(func()) def sequence(func): -- cgit v1.2.3 From 37621846d541d7afcad52e6dba8281bd1146cf09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sat, 2 Mar 2013 01:33:53 +0100 Subject: Deprecate the extract_prefix option to PostGeneration. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduces a new, call-less syntax for the @post_generation decorator. Signed-off-by: Raphaël Barrois --- factory/declarations.py | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) (limited to 'factory') diff --git a/factory/declarations.py b/factory/declarations.py index d5b7950..83f4d32 100644 --- a/factory/declarations.py +++ b/factory/declarations.py @@ -363,6 +363,11 @@ class PostGenerationDeclaration(object): """ def __init__(self, extract_prefix=None): + if extract_prefix: + warnings.warn( + "The extract_prefix argument to PostGeneration declarations " + "is deprecated and will be removed in the future.", + PendingDeprecationWarning, 3) self.extract_prefix = extract_prefix def extract(self, name, attrs): @@ -410,10 +415,22 @@ class PostGeneration(PostGenerationDeclaration): self.function(obj, create, extracted, **kwargs) -def post_generation(extract_prefix=None): - def decorator(fun): - return PostGeneration(fun, extract_prefix=extract_prefix) - return decorator +def post_generation(*args, **kwargs): + assert len(args) + len(kwargs) <= 1, "post_generation takes at most one argument." + if args and callable(args[0]): + # Called as @post_generation applied to a function + return PostGeneration(args[0]) + else: + warnings.warn( + "The @post_generation should now be applied directly to the " + "function, without parameters. The @post_generation() and " + "@post_generation(extract_prefix='xxx') syntaxes are deprecated " + "and will be removed in the future; use @post_generation instead.", + PendingDeprecationWarning, 2) + extract_prefix = kwargs.get('extract_prefix') + def decorator(fun): + return PostGeneration(fun, extract_prefix=extract_prefix) + return decorator class RelatedFactory(PostGenerationDeclaration): -- cgit v1.2.3 From e8dc25b5db5b470a64cc6a89259d476269fcebb5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sun, 3 Mar 2013 20:59:58 +0100 Subject: Get rid of the FACTORY_ABSTRACT rename. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This was just adding noise to an already complex release. Signed-off-by: Raphaël Barrois --- factory/base.py | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) (limited to 'factory') diff --git a/factory/base.py b/factory/base.py index c8c7f06..28d7cdb 100644 --- a/factory/base.py +++ b/factory/base.py @@ -212,21 +212,12 @@ class FactoryMetaClass(BaseFactoryMetaClass): for construction of an associated class instance at a later time.""" parent_factories = get_factory_bases(bases) - if not parent_factories or attrs.get('ABSTRACT_FACTORY', False) \ - or attrs.get('FACTORY_ABSTRACT', False): + if not parent_factories or attrs.get('ABSTRACT_FACTORY', False): # If this isn't a subclass of Factory, or specifically declared # abstract, don't do anything special. if 'ABSTRACT_FACTORY' in attrs: - warnings.warn( - "The 'ABSTRACT_FACTORY' class attribute has been renamed " - "to 'FACTORY_ABSTRACT' for naming consistency, and will " - "be ignored in the future. Please upgrade class %s." % - class_name, DeprecationWarning, 2) attrs.pop('ABSTRACT_FACTORY') - if 'FACTORY_ABSTRACT' in attrs: - attrs.pop('FACTORY_ABSTRACT') - return super(FactoryMetaClass, cls).__new__(cls, class_name, bases, attrs) base = parent_factories[0] @@ -650,7 +641,7 @@ class DjangoModelFactory(Factory): handle those for non-numerical primary keys. """ - FACTORY_ABSTRACT = True + ABSTRACT_FACTORY = True @classmethod def _setup_next_sequence(cls): @@ -669,7 +660,7 @@ class DjangoModelFactory(Factory): class MogoFactory(Factory): """Factory for mogo objects.""" - FACTORY_ABSTRACT = True + ABSTRACT_FACTORY = True @classmethod def _build(cls, target_class, *args, **kwargs): -- cgit v1.2.3 From 9422cf12516143650f1014f34f996260c00d4c0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sun, 3 Mar 2013 22:10:42 +0100 Subject: Allow symbol names in RelatedFactory (Closes #30). MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This works exactly as for SubFactory. Signed-off-by: Raphaël Barrois --- factory/declarations.py | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) (limited to 'factory') diff --git a/factory/declarations.py b/factory/declarations.py index 83f4d32..d3d7659 100644 --- a/factory/declarations.py +++ b/factory/declarations.py @@ -445,16 +445,37 @@ class RelatedFactory(PostGenerationDeclaration): def __init__(self, factory, name='', **defaults): super(RelatedFactory, self).__init__(extract_prefix=None) - self.factory = factory self.name = name self.defaults = defaults + if isinstance(factory, type): + self.factory = factory + self.factory_module = self.factory_name = '' + else: + # Must be a string + if not isinstance(factory, basestring) or '.' not in factory: + raise ValueError( + "The argument of a SubFactory must be either a class " + "or the fully qualified path to a Factory class; got " + "%r instead." % factory) + self.factory = None + self.factory_module, self.factory_name = factory.rsplit('.', 1) + + def get_factory(self): + """Retrieve the wrapped factory.Factory subclass.""" + if self.factory is None: + # Must be a module path + self.factory = utils.import_object(self.factory_module, self.factory_name) + return self.factory + def call(self, obj, create, extracted=None, **kwargs): passed_kwargs = dict(self.defaults) passed_kwargs.update(kwargs) if self.name: passed_kwargs[self.name] = obj - self.factory.simple_generate(create, **passed_kwargs) + + factory = self.get_factory() + factory.simple_generate(create, **passed_kwargs) class PostGenerationMethodCall(PostGenerationDeclaration): -- cgit v1.2.3 From 7d792430e103984a91c102c33da79be2426bc632 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Mon, 4 Mar 2013 23:18:02 +0100 Subject: Add a 'after post_generation' hook to Factory. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Use it in DjangoModelFactory to save objects again if a post_generation hook ran. Signed-off-by: Raphaël Barrois --- factory/base.py | 24 +++++++++++++++++++++++- factory/declarations.py | 2 +- 2 files changed, 24 insertions(+), 2 deletions(-) (limited to 'factory') diff --git a/factory/base.py b/factory/base.py index 28d7cdb..3ebc746 100644 --- a/factory/base.py +++ b/factory/base.py @@ -616,11 +616,26 @@ class Factory(BaseFactory): obj = cls._prepare(create, **attrs) # Handle post-generation attributes + results = {} for name, decl in sorted(postgen_declarations.items()): extracted, extracted_kwargs = postgen_attributes[name] - decl.call(obj, create, extracted, **extracted_kwargs) + results[name] = decl.call(obj, create, extracted, **extracted_kwargs) + + cls._after_postgeneration(obj, create, results) + return obj + @classmethod + def _after_postgeneration(cls, obj, create, results=None): + """Hook called after post-generation declarations have been handled. + + Args: + obj (object): the generated object + create (bool): whether the strategy was 'build' or 'create' + results (dict or None): result of post-generation declarations + """ + pass + @classmethod def build(cls, **kwargs): attrs = cls.attributes(create=False, extra=kwargs) @@ -657,6 +672,13 @@ class DjangoModelFactory(Factory): """Create an instance of the model, and save it to the database.""" return target_class._default_manager.create(*args, **kwargs) + @classmethod + def _after_postgeneration(cls, obj, create, results=None): + """Save again the instance if creating and at least one hook ran.""" + if create and results: + # Some post-generation hooks ran, and may have modified us. + obj.save() + class MogoFactory(Factory): """Factory for mogo objects.""" diff --git a/factory/declarations.py b/factory/declarations.py index d3d7659..366c2c8 100644 --- a/factory/declarations.py +++ b/factory/declarations.py @@ -412,7 +412,7 @@ class PostGeneration(PostGenerationDeclaration): self.function = function def call(self, obj, create, extracted=None, **kwargs): - self.function(obj, create, extracted, **kwargs) + return self.function(obj, create, extracted, **kwargs) def post_generation(*args, **kwargs): -- cgit v1.2.3 From be403fd5a109af49d228ab620ab14d04cb9e34c8 Mon Sep 17 00:00:00 2001 From: Chris Lasher Date: Thu, 17 Jan 2013 16:29:14 -0500 Subject: Use extracted argument in PostGenerationMethodCall. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This changeset makes it possible possible to override the default method arguments (or "method_args") passed in when instantiating PostGenerationMethodCall. Now the user can override the default arguments to the method called during post-generation when instantiating a factory. For example, using this UserFactory, class UserFactory(factory.Factory): FACTORY_FOR = User username = factory.Sequence(lambda n: 'user{0}'.format(n)) password = factory.PostGenerationMethodCall( 'set_password', None, 'defaultpassword') by default, the user will have a password set to 'defaultpassword', but this can be overridden by passing in a new password as a keyword argument: >>> u = UserFactory() >>> u.check_password('defaultpassword') True >>> other_u = UserFactory(password='different') >>> other_u.check_password('defaultpassword') False >>> other_u.check_password('different') True This changeset introduces a testing dependency on the Mock package http://pypi.python.org/pypi/mock. While this is a third-party dependency in Python 2, it is part of the Python 3 standard library, as unit.mock, and so a reasonable dependency to satisfy. Signed-off-by: Raphaël Barrois --- factory/declarations.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) (limited to 'factory') diff --git a/factory/declarations.py b/factory/declarations.py index 366c2c8..1f1d2af 100644 --- a/factory/declarations.py +++ b/factory/declarations.py @@ -21,6 +21,7 @@ # THE SOFTWARE. +import collections import itertools import warnings @@ -498,10 +499,17 @@ class PostGenerationMethodCall(PostGenerationDeclaration): self.method_kwargs = kwargs def call(self, obj, create, extracted=None, **kwargs): + if extracted is not None: + passed_args = extracted + if isinstance(passed_args, basestring) or ( + not isinstance(passed_args, collections.Iterable)): + passed_args = (passed_args,) + else: + passed_args = self.method_args passed_kwargs = dict(self.method_kwargs) passed_kwargs.update(kwargs) method = getattr(obj, self.method_name) - method(*self.method_args, **passed_kwargs) + method(*passed_args, **passed_kwargs) # Decorators... in case lambdas don't cut it -- cgit v1.2.3 From 2bc0fc8413c02a7faf3a116fe875d76bc3403117 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Tue, 5 Mar 2013 00:36:08 +0100 Subject: Cleanup argument extraction in PostGenMethod (See #36). MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This provides a consistent behaviour for extracting arguments to a PostGenerationMethodCall. Signed-off-by: Raphaël Barrois --- factory/declarations.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) (limited to 'factory') diff --git a/factory/declarations.py b/factory/declarations.py index 1f1d2af..efaadbe 100644 --- a/factory/declarations.py +++ b/factory/declarations.py @@ -492,20 +492,23 @@ class PostGenerationMethodCall(PostGenerationDeclaration): ... password = factory.PostGenerationMethodCall('set_password', password='') """ - def __init__(self, method_name, extract_prefix=None, *args, **kwargs): + def __init__(self, method_name, *args, **kwargs): + extract_prefix = kwargs.pop('extract_prefix', None) super(PostGenerationMethodCall, self).__init__(extract_prefix) self.method_name = method_name self.method_args = args self.method_kwargs = kwargs def call(self, obj, create, extracted=None, **kwargs): - if extracted is not None: - passed_args = extracted - if isinstance(passed_args, basestring) or ( - not isinstance(passed_args, collections.Iterable)): - passed_args = (passed_args,) - else: + if extracted is None: passed_args = self.method_args + + elif len(self.method_args) <= 1: + # Max one argument expected + passed_args = (extracted,) + else: + passed_args = tuple(extracted) + passed_kwargs = dict(self.method_kwargs) passed_kwargs.update(kwargs) method = getattr(obj, self.method_name) -- cgit v1.2.3 From 114ac649e448e97a210cf8dccc6ba50278645ce6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Tue, 5 Mar 2013 23:12:10 +0100 Subject: Stop calling Foo.objects.create() when it doesn't break (Closes #23). MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This will be properly fixed in v2.0.0; the current heuristic is: - If the user defined a custom _create method, use it - If he didn't, but the associated class has a objects attribute, use TheClass.objects.create(*args, **kwargs) - Otherwise, simply call TheClass(*args, **kwargs). Signed-off-by: Raphaël Barrois --- factory/base.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'factory') diff --git a/factory/base.py b/factory/base.py index 3ebc746..44d58a7 100644 --- a/factory/base.py +++ b/factory/base.py @@ -527,7 +527,8 @@ class Factory(BaseFactory): creation_function = getattr(cls, '_creation_function', ()) if creation_function and creation_function[0]: return creation_function[0] - elif cls._create.__func__ == Factory._create.__func__: + elif cls._create.__func__ == Factory._create.__func__ and \ + hasattr(cls._associated_class, 'objects'): # Backwards compatibility. # Default creation_function and default _create() behavior. # The best "Vanilla" _create detection algorithm I found is relying -- cgit v1.2.3 From 17097d23986b43bb74421dcbb0a0f5d75433114c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Mon, 11 Mar 2013 17:21:18 +0100 Subject: Version bump to 1.3.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Raphaël Barrois --- factory/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'factory') diff --git a/factory/__init__.py b/factory/__init__.py index 0cf1b82..6c56955 100644 --- a/factory/__init__.py +++ b/factory/__init__.py @@ -20,7 +20,7 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. -__version__ = '1.3.0-dev' +__version__ = '1.3.0' __author__ = 'Raphaël Barrois ' from .base import ( -- cgit v1.2.3 From 91592efbdff6de4b21b3a0b1a4d86dff8818f580 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Mon, 11 Mar 2013 22:07:01 +0100 Subject: Proper manager fetching in DjangoModelFactory. --- factory/base.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) (limited to 'factory') diff --git a/factory/base.py b/factory/base.py index 44d58a7..e798c68 100644 --- a/factory/base.py +++ b/factory/base.py @@ -659,11 +659,21 @@ class DjangoModelFactory(Factory): ABSTRACT_FACTORY = True + @classmethod + def _get_manager(cls, target_class): + try: + return target_class._default_manager + except AttributeError: + return target_class.objects + @classmethod def _setup_next_sequence(cls): """Compute the next available PK, based on the 'pk' database field.""" + + manager = cls._get_manager(cls._associated_class) + try: - return 1 + cls._associated_class._default_manager.values_list('pk', flat=True + return 1 + manager.values_list('pk', flat=True ).order_by('-pk')[0] except IndexError: return 1 @@ -671,7 +681,8 @@ class DjangoModelFactory(Factory): @classmethod def _create(cls, target_class, *args, **kwargs): """Create an instance of the model, and save it to the database.""" - return target_class._default_manager.create(*args, **kwargs) + manager = cls._get_manager(target_class) + return manager.create(*args, **kwargs) @classmethod def _after_postgeneration(cls, obj, create, results=None): -- cgit v1.2.3 From ba1fd987dad9268a1e5a41fe10513727aadfd9b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Mon, 11 Mar 2013 22:07:23 +0100 Subject: Add FACTORY_CLASS kwarg to make_factory and friends. --- factory/base.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'factory') diff --git a/factory/base.py b/factory/base.py index e798c68..a6ab98e 100644 --- a/factory/base.py +++ b/factory/base.py @@ -705,7 +705,9 @@ def make_factory(klass, **kwargs): """Create a new, simple factory for the given class.""" factory_name = '%sFactory' % klass.__name__ kwargs[FACTORY_CLASS_DECLARATION] = klass - factory_class = type(Factory).__new__(type(Factory), factory_name, (Factory,), kwargs) + base_class = kwargs.pop('FACTORY_CLASS', Factory) + + factory_class = type(Factory).__new__(type(Factory), factory_name, (base_class,), kwargs) factory_class.__name__ = '%sFactory' % klass.__name__ factory_class.__doc__ = 'Auto-generated factory for class %s' % klass return factory_class -- cgit v1.2.3 From 7121fbe268b366bf543b9862ff384453edbce414 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Mon, 11 Mar 2013 22:12:43 +0100 Subject: Remove building_function/creation_function. Stop defaulting to Django's .objects.create(). --- factory/__init__.py | 4 --- factory/base.py | 92 ----------------------------------------------------- 2 files changed, 96 deletions(-) (limited to 'factory') diff --git a/factory/__init__.py b/factory/__init__.py index 6c56955..ab8005f 100644 --- a/factory/__init__.py +++ b/factory/__init__.py @@ -45,10 +45,6 @@ from .base import ( CREATE_STRATEGY, STUB_STRATEGY, use_strategy, - - DJANGO_CREATION, - NAIVE_BUILD, - MOGO_BUILD, ) from .declarations import ( diff --git a/factory/base.py b/factory/base.py index a6ab98e..06730ba 100644 --- a/factory/base.py +++ b/factory/base.py @@ -31,20 +31,6 @@ BUILD_STRATEGY = 'build' CREATE_STRATEGY = 'create' STUB_STRATEGY = 'stub' -# Creation functions. Deprecated. -# Override Factory._create instead. -def DJANGO_CREATION(class_to_create, **kwargs): - warnings.warn( - "Factories defaulting to Django's Foo.objects.create() is deprecated, " - "and will be removed in the future. Please inherit from " - "factory.DjangoModelFactory instead.", PendingDeprecationWarning, 6) - return class_to_create.objects.create(**kwargs) - -# Building functions. Deprecated. -# Override Factory._build instead. -NAIVE_BUILD = lambda class_to_build, **kwargs: class_to_build(**kwargs) -MOGO_BUILD = lambda class_to_build, **kwargs: class_to_build.new(**kwargs) - # Special declarations FACTORY_CLASS_DECLARATION = 'FACTORY_FOR' @@ -497,76 +483,6 @@ class Factory(BaseFactory): class AssociatedClassError(RuntimeError): pass - @classmethod - def set_creation_function(cls, creation_function): - """Set the creation function for this class. - - Args: - creation_function (function): the new creation function. That - function should take one non-keyword argument, the 'class' for - which an instance will be created. The value of the various - fields are passed as keyword arguments. - """ - warnings.warn( - "Use of factory.set_creation_function is deprecated, and will be " - "removed in the future. Please override Factory._create() instead.", - PendingDeprecationWarning, 2) - # Customizing 'create' strategy, using a tuple to keep the creation function - # from turning it into an instance method. - cls._creation_function = (creation_function,) - - @classmethod - def get_creation_function(cls): - """Retrieve the creation function for this class. - - Returns: - function: A function that takes one parameter, the class for which - an instance will be created, and keyword arguments for the value - of the fields of the instance. - """ - creation_function = getattr(cls, '_creation_function', ()) - if creation_function and creation_function[0]: - return creation_function[0] - elif cls._create.__func__ == Factory._create.__func__ and \ - hasattr(cls._associated_class, 'objects'): - # Backwards compatibility. - # Default creation_function and default _create() behavior. - # The best "Vanilla" _create detection algorithm I found is relying - # on actual method implementation (otherwise, make_factory isn't - # detected as 'default'). - return DJANGO_CREATION - - @classmethod - def set_building_function(cls, building_function): - """Set the building function for this class. - - Args: - building_function (function): the new building function. That - function should take one non-keyword argument, the 'class' for - which an instance will be built. The value of the various - fields are passed as keyword arguments. - """ - warnings.warn( - "Use of factory.set_building_function is deprecated, and will be " - "removed in the future. Please override Factory._build() instead.", - PendingDeprecationWarning, 2) - # Customizing 'build' strategy, using a tuple to keep the creation function - # from turning it into an instance method. - cls._building_function = (building_function,) - - @classmethod - def get_building_function(cls): - """Retrieve the building function for this class. - - Returns: - function: A function that takes one parameter, the class for which - an instance will be created, and keyword arguments for the value - of the fields of the instance. - """ - building_function = getattr(cls, '_building_function', ()) - if building_function and building_function[0]: - return building_function[0] - @classmethod def _adjust_kwargs(cls, **kwargs): """Extension point for custom kwargs adjustment.""" @@ -587,16 +503,8 @@ class Factory(BaseFactory): args = tuple(kwargs.pop(key) for key in cls.FACTORY_ARG_PARAMETERS) if create: - # Backwards compatibility - creation_function = cls.get_creation_function() - if creation_function: - return creation_function(target_class, *args, **kwargs) return cls._create(target_class, *args, **kwargs) else: - # Backwards compatibility - building_function = cls.get_building_function() - if building_function: - return building_function(target_class, *args, **kwargs) return cls._build(target_class, *args, **kwargs) @classmethod -- cgit v1.2.3 From e8327fcb2e31dd7a9ffc7f53c7a678d1c1135cb2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Mon, 11 Mar 2013 22:22:31 +0100 Subject: Start work on v2. --- factory/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'factory') diff --git a/factory/__init__.py b/factory/__init__.py index ab8005f..db88f4e 100644 --- a/factory/__init__.py +++ b/factory/__init__.py @@ -20,7 +20,7 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. -__version__ = '1.3.0' +__version__ = '2.0.0-dev' __author__ = 'Raphaël Barrois ' from .base import ( -- cgit v1.2.3 From f4100b373418a58dba7ff4f29cfb44df4eca3d15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Mon, 11 Mar 2013 22:17:57 +0100 Subject: Remove automagic associated class discovery. --- factory/base.py | 36 ++++-------------------------------- 1 file changed, 4 insertions(+), 32 deletions(-) (limited to 'factory') diff --git a/factory/base.py b/factory/base.py index 06730ba..8bb6d95 100644 --- a/factory/base.py +++ b/factory/base.py @@ -151,7 +151,6 @@ class FactoryMetaClass(BaseFactoryMetaClass): to a class. """ own_associated_class = None - used_auto_discovery = False if FACTORY_CLASS_DECLARATION in attrs: return attrs[FACTORY_CLASS_DECLARATION] @@ -161,37 +160,10 @@ class FactoryMetaClass(BaseFactoryMetaClass): if inherited is not None: return inherited - if '__module__' in attrs: - factory_module = sys.modules[attrs['__module__']] - if class_name.endswith('Factory'): - # Try a module lookup - used_auto_discovery = True - associated_name = class_name[:-len('Factory')] - if associated_name and hasattr(factory_module, associated_name): - warnings.warn( - "Auto-discovery of associated class is deprecated, and " - "will be removed in the future. Please set '%s = %s' " - "in the %s class definition." % ( - FACTORY_CLASS_DECLARATION, - associated_name, - class_name, - ), DeprecationWarning, 3) - - return getattr(factory_module, associated_name) - - # Unable to guess a good option; return the inherited class. - # Unable to find an associated class; fail. - if used_auto_discovery: - raise Factory.AssociatedClassError( - FactoryMetaClass.ERROR_MESSAGE_AUTODISCOVERY.format( - FACTORY_CLASS_DECLARATION, - associated_name, - class_name, - factory_module,)) - else: - raise Factory.AssociatedClassError( - FactoryMetaClass.ERROR_MESSAGE.format( - FACTORY_CLASS_DECLARATION)) + raise Factory.AssociatedClassError( + "Could not determine the class associated with %s. " + "Use the FACTORY_FOR attribute to specify an associated class." % + class_name) def __new__(cls, class_name, bases, attrs): """Determine the associated class based on the factory class name. Record the associated class -- cgit v1.2.3 From de3a552eab032cb980a2fb78976fc3dc8cd5f1c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Mon, 11 Mar 2013 22:28:43 +0100 Subject: Remove InfiniteIterator and infinite_iterator. Use Iterator/iterator instead. --- factory/__init__.py | 2 -- factory/declarations.py | 23 ----------------------- 2 files changed, 25 deletions(-) (limited to 'factory') diff --git a/factory/__init__.py b/factory/__init__.py index db88f4e..4b4857c 100644 --- a/factory/__init__.py +++ b/factory/__init__.py @@ -50,7 +50,6 @@ from .base import ( from .declarations import ( LazyAttribute, Iterator, - InfiniteIterator, Sequence, LazyAttributeSequence, SelfAttribute, @@ -63,7 +62,6 @@ from .declarations import ( lazy_attribute, iterator, - infinite_iterator, sequence, lazy_attribute_sequence, container_attribute, diff --git a/factory/declarations.py b/factory/declarations.py index efaadbe..1f64038 100644 --- a/factory/declarations.py +++ b/factory/declarations.py @@ -156,21 +156,6 @@ class Iterator(OrderedDeclaration): return self.getter(value) -class InfiniteIterator(Iterator): - """Same as Iterator, but make the iterator infinite by cycling at the end. - - Attributes: - iterator (iterable): the iterator, once made infinite. - """ - - def __init__(self, iterator): - warnings.warn( - "factory.InfiniteIterator is deprecated, and will be removed in the " - "future. Please use factory.Iterator instead.", - PendingDeprecationWarning, 2) - return super(InfiniteIterator, self).__init__(iterator, cycle=True) - - class Sequence(OrderedDeclaration): """Specific OrderedDeclaration to use for 'sequenced' fields. @@ -524,14 +509,6 @@ def iterator(func): """Turn a generator function into an iterator attribute.""" return Iterator(func()) -def infinite_iterator(func): - """Turn a generator function into an infinite iterator attribute.""" - warnings.warn( - "@factory.infinite_iterator is deprecated and will be removed in the " - "future. Please use @factory.iterator instead.", - PendingDeprecationWarning, 2) - return InfiniteIterator(func()) - def sequence(func): return Sequence(func) -- cgit v1.2.3 From 16e1a65f5b93615d946b74e3fb4d0b61c99ae0d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Mon, 11 Mar 2013 22:30:45 +0100 Subject: Remove CircularSubFactory. Replace CircularSubFactory('module', 'symbol') with SubFactory('module.symbol'). --- factory/__init__.py | 1 - factory/declarations.py | 14 -------------- 2 files changed, 15 deletions(-) (limited to 'factory') diff --git a/factory/__init__.py b/factory/__init__.py index 4b4857c..adcf9c9 100644 --- a/factory/__init__.py +++ b/factory/__init__.py @@ -55,7 +55,6 @@ from .declarations import ( SelfAttribute, ContainerAttribute, SubFactory, - CircularSubFactory, PostGeneration, PostGenerationMethodCall, RelatedFactory, diff --git a/factory/declarations.py b/factory/declarations.py index 1f64038..b3c9d6a 100644 --- a/factory/declarations.py +++ b/factory/declarations.py @@ -325,20 +325,6 @@ class SubFactory(ParameteredAttribute): return subfactory.build(**params) -class CircularSubFactory(SubFactory): - """Use to solve circular dependencies issues.""" - def __init__(self, module_name, factory_name, **kwargs): - factory = '%s.%s' % (module_name, factory_name) - warnings.warn( - "factory.CircularSubFactory is deprecated and will be removed in " - "the future. " - "Please replace factory.CircularSubFactory('module', 'symbol') " - "with factory.SubFactory('module.symbol').", - PendingDeprecationWarning, 2) - - super(CircularSubFactory, self).__init__(factory, **kwargs) - - class PostGenerationDeclaration(object): """Declarations to be called once the target object has been generated. -- cgit v1.2.3 From 60f0969406bd349a8a8b88fcaec819fa5c0525cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Mon, 11 Mar 2013 22:36:30 +0100 Subject: Remove extract_prefix from post-generation hooks. Magic abuse is bad. --- factory/declarations.py | 53 ++++++++++--------------------------------------- 1 file changed, 10 insertions(+), 43 deletions(-) (limited to 'factory') diff --git a/factory/declarations.py b/factory/declarations.py index b3c9d6a..b491bfb 100644 --- a/factory/declarations.py +++ b/factory/declarations.py @@ -326,21 +326,7 @@ class SubFactory(ParameteredAttribute): class PostGenerationDeclaration(object): - """Declarations to be called once the target object has been generated. - - Attributes: - extract_prefix (str): prefix to use when extracting attributes from - the factory's declaration for this declaration. If empty, uses - the attribute name of the PostGenerationDeclaration. - """ - - def __init__(self, extract_prefix=None): - if extract_prefix: - warnings.warn( - "The extract_prefix argument to PostGeneration declarations " - "is deprecated and will be removed in the future.", - PendingDeprecationWarning, 3) - self.extract_prefix = extract_prefix + """Declarations to be called once the target object has been generated.""" def extract(self, name, attrs): """Extract relevant attributes from a dict. @@ -355,12 +341,8 @@ class PostGenerationDeclaration(object): (object, dict): a tuple containing the attribute at 'name' (if provided) and a dict of extracted attributes """ - if self.extract_prefix: - extract_prefix = self.extract_prefix - else: - extract_prefix = name - extracted = attrs.pop(extract_prefix, None) - kwargs = utils.extract_dict(extract_prefix, attrs) + extracted = attrs.pop(name, None) + kwargs = utils.extract_dict(name, attrs) return extracted, kwargs def call(self, obj, create, extracted=None, **kwargs): # pragma: no cover @@ -369,7 +351,7 @@ class PostGenerationDeclaration(object): Args: obj (object): the newly generated object create (bool): whether the object was 'built' or 'created' - extracted (object): the value given for in the + extracted (object): the value given for in the object definition, or None if not provided. kwargs (dict): declarations extracted from the object definition for this hook @@ -379,30 +361,16 @@ class PostGenerationDeclaration(object): class PostGeneration(PostGenerationDeclaration): """Calls a given function once the object has been generated.""" - def __init__(self, function, extract_prefix=None): - super(PostGeneration, self).__init__(extract_prefix) + def __init__(self, function): + super(PostGeneration, self).__init__() self.function = function def call(self, obj, create, extracted=None, **kwargs): return self.function(obj, create, extracted, **kwargs) -def post_generation(*args, **kwargs): - assert len(args) + len(kwargs) <= 1, "post_generation takes at most one argument." - if args and callable(args[0]): - # Called as @post_generation applied to a function - return PostGeneration(args[0]) - else: - warnings.warn( - "The @post_generation should now be applied directly to the " - "function, without parameters. The @post_generation() and " - "@post_generation(extract_prefix='xxx') syntaxes are deprecated " - "and will be removed in the future; use @post_generation instead.", - PendingDeprecationWarning, 2) - extract_prefix = kwargs.get('extract_prefix') - def decorator(fun): - return PostGeneration(fun, extract_prefix=extract_prefix) - return decorator +def post_generation(fun): + return PostGeneration(fun) class RelatedFactory(PostGenerationDeclaration): @@ -416,7 +384,7 @@ class RelatedFactory(PostGenerationDeclaration): """ def __init__(self, factory, name='', **defaults): - super(RelatedFactory, self).__init__(extract_prefix=None) + super(RelatedFactory, self).__init__() self.name = name self.defaults = defaults @@ -464,8 +432,7 @@ class PostGenerationMethodCall(PostGenerationDeclaration): password = factory.PostGenerationMethodCall('set_password', password='') """ def __init__(self, method_name, *args, **kwargs): - extract_prefix = kwargs.pop('extract_prefix', None) - super(PostGenerationMethodCall, self).__init__(extract_prefix) + super(PostGenerationMethodCall, self).__init__() self.method_name = method_name self.method_args = args self.method_kwargs = kwargs -- cgit v1.2.3 From d63821daba2002b8c455777748007f7198d3d3bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Fri, 15 Mar 2013 01:08:00 +0100 Subject: Remove unused constants. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Raphaël Barrois --- factory/base.py | 6 ------ 1 file changed, 6 deletions(-) (limited to 'factory') diff --git a/factory/base.py b/factory/base.py index 8bb6d95..25d7a14 100644 --- a/factory/base.py +++ b/factory/base.py @@ -120,12 +120,6 @@ class BaseFactoryMetaClass(type): class FactoryMetaClass(BaseFactoryMetaClass): """Factory metaclass for handling class association and ordered declarations.""" - ERROR_MESSAGE = """Could not determine what class this factory is for. - Use the {0} attribute to specify a class.""" - ERROR_MESSAGE_AUTODISCOVERY = ERROR_MESSAGE + """ - Also, autodiscovery failed using the name '{1}' - based on the Factory name '{2}' in {3}.""" - @classmethod def _discover_associated_class(cls, class_name, attrs, inherited=None): """Try to find the class associated with this factory. -- cgit v1.2.3 From 6e9bf5af909e1e164a294fd5589edc4fada06731 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Fri, 15 Mar 2013 01:33:56 +0100 Subject: Merge BaseFactoryMetaClass into FactoryMetaClass. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Also fix FACTORY_STRATEGY. Signed-off-by: Raphaël Barrois --- factory/base.py | 200 +++++++++++++++++++++++++++++--------------------------- 1 file changed, 105 insertions(+), 95 deletions(-) (limited to 'factory') diff --git a/factory/base.py b/factory/base.py index 25d7a14..aef21d5 100644 --- a/factory/base.py +++ b/factory/base.py @@ -44,11 +44,11 @@ CLASS_ATTRIBUTE_ASSOCIATED_CLASS = '_associated_class' # Factory metaclasses def get_factory_bases(bases): - """Retrieve all BaseFactoryMetaClass-derived bases from a list.""" - return [b for b in bases if isinstance(b, BaseFactoryMetaClass)] + """Retrieve all FactoryMetaClass-derived bases from a list.""" + return [b for b in bases if issubclass(b, BaseFactory)] -class BaseFactoryMetaClass(type): +class FactoryMetaClass(type): """Factory metaclass for handling ordered declarations.""" def __call__(cls, **kwargs): @@ -57,68 +57,14 @@ class BaseFactoryMetaClass(type): Returns an instance of the associated class. """ - if cls.default_strategy == BUILD_STRATEGY: + if cls.FACTORY_STRATEGY == BUILD_STRATEGY: return cls.build(**kwargs) - elif cls.default_strategy == CREATE_STRATEGY: + elif cls.FACTORY_STRATEGY == CREATE_STRATEGY: return cls.create(**kwargs) - elif cls.default_strategy == STUB_STRATEGY: + elif cls.FACTORY_STRATEGY == STUB_STRATEGY: return cls.stub(**kwargs) else: - raise BaseFactory.UnknownStrategy('Unknown default_strategy: {0}'.format(cls.default_strategy)) - - def __new__(cls, class_name, bases, attrs, extra_attrs=None): - """Record attributes as a pattern for later instance construction. - - This is called when a new Factory subclass is defined; it will collect - attribute declaration from the class definition. - - Args: - class_name (str): the name of the class being created - bases (list of class): the parents of the class being created - attrs (str => obj dict): the attributes as defined in the class - definition - extra_attrs (str => obj dict): extra attributes that should not be - included in the factory defaults, even if public. This - argument is only provided by extensions of this metaclass. - - Returns: - A new class - """ - - parent_factories = get_factory_bases(bases) - if not parent_factories: - # If this isn't a subclass of Factory, don't do anything special. - return super(BaseFactoryMetaClass, cls).__new__(cls, class_name, bases, attrs) - - declarations = containers.DeclarationDict() - postgen_declarations = containers.PostGenerationDeclarationDict() - - # Add parent declarations in reverse order. - for base in reversed(parent_factories): - # Import parent PostGenerationDeclaration - postgen_declarations.update_with_public( - getattr(base, CLASS_ATTRIBUTE_POSTGEN_DECLARATIONS, {})) - # Import all 'public' attributes (avoid those starting with _) - declarations.update_with_public(getattr(base, CLASS_ATTRIBUTE_DECLARATIONS, {})) - - # Import attributes from the class definition - non_postgen_attrs = postgen_declarations.update_with_public(attrs) - # Store protected/private attributes in 'non_factory_attrs'. - non_factory_attrs = declarations.update_with_public(non_postgen_attrs) - - # Store the DeclarationDict in the attributes of the newly created class - non_factory_attrs[CLASS_ATTRIBUTE_DECLARATIONS] = declarations - non_factory_attrs[CLASS_ATTRIBUTE_POSTGEN_DECLARATIONS] = postgen_declarations - - # Add extra args if provided. - if extra_attrs: - non_factory_attrs.update(extra_attrs) - - return super(BaseFactoryMetaClass, cls).__new__(cls, class_name, bases, non_factory_attrs) - - -class FactoryMetaClass(BaseFactoryMetaClass): - """Factory metaclass for handling class association and ordered declarations.""" + raise BaseFactory.UnknownStrategy('Unknown FACTORY_STRATEGY: {0}'.format(cls.FACTORY_STRATEGY)) @classmethod def _discover_associated_class(cls, class_name, attrs, inherited=None): @@ -126,8 +72,6 @@ class FactoryMetaClass(BaseFactoryMetaClass): In order, the following tests will be performed: - Lookup the FACTORY_CLASS_DECLARATION attribute - - If the newly created class is named 'FooBarFactory', look for a FooBar - class in its module - If an inherited associated class was provided, use it. Args: @@ -154,46 +98,103 @@ class FactoryMetaClass(BaseFactoryMetaClass): if inherited is not None: return inherited - raise Factory.AssociatedClassError( + raise AssociatedClassError( "Could not determine the class associated with %s. " "Use the FACTORY_FOR attribute to specify an associated class." % class_name) - def __new__(cls, class_name, bases, attrs): - """Determine the associated class based on the factory class name. Record the associated class - for construction of an associated class instance at a later time.""" + @classmethod + def _extract_declarations(cls, bases, attributes): + """Extract declarations from a class definition. - parent_factories = get_factory_bases(bases) - if not parent_factories or attrs.get('ABSTRACT_FACTORY', False): - # If this isn't a subclass of Factory, or specifically declared - # abstract, don't do anything special. - if 'ABSTRACT_FACTORY' in attrs: - attrs.pop('ABSTRACT_FACTORY') + Args: + bases (class list): parent Factory subclasses + attributes (dict): attributes declared in the class definition + Returns: + dict: the original attributes, where declarations have been moved to + _declarations and post-generation declarations to + _postgen_declarations. + """ + declarations = containers.DeclarationDict() + postgen_declarations = containers.PostGenerationDeclarationDict() + + # Add parent declarations in reverse order. + for base in reversed(bases): + # Import parent PostGenerationDeclaration + postgen_declarations.update_with_public( + getattr(base, CLASS_ATTRIBUTE_POSTGEN_DECLARATIONS, {})) + # Import all 'public' attributes (avoid those starting with _) + declarations.update_with_public(getattr(base, CLASS_ATTRIBUTE_DECLARATIONS, {})) + + # Import attributes from the class definition + attributes = postgen_declarations.update_with_public(attributes) + # Store protected/private attributes in 'non_factory_attrs'. + attributes = declarations.update_with_public(attributes) + + # Store the DeclarationDict in the attributes of the newly created class + attributes[CLASS_ATTRIBUTE_DECLARATIONS] = declarations + attributes[CLASS_ATTRIBUTE_POSTGEN_DECLARATIONS] = postgen_declarations + + return attributes + + def __new__(cls, class_name, bases, attrs, extra_attrs=None): + """Record attributes as a pattern for later instance construction. + + This is called when a new Factory subclass is defined; it will collect + attribute declaration from the class definition. + + Args: + class_name (str): the name of the class being created + bases (list of class): the parents of the class being created + attrs (str => obj dict): the attributes as defined in the class + definition + extra_attrs (str => obj dict): extra attributes that should not be + included in the factory defaults, even if public. This + argument is only provided by extensions of this metaclass. + + Returns: + A new class + """ + parent_factories = get_factory_bases(bases) + if not parent_factories: return super(FactoryMetaClass, cls).__new__(cls, class_name, bases, attrs) - base = parent_factories[0] + is_abstract = attrs.pop('ABSTRACT_FACTORY', False) + extra_attrs = {} + + if not is_abstract: + + base = parent_factories[0] - inherited_associated_class = getattr(base, - CLASS_ATTRIBUTE_ASSOCIATED_CLASS, None) - associated_class = cls._discover_associated_class(class_name, attrs, - inherited_associated_class) + inherited_associated_class = getattr(base, + CLASS_ATTRIBUTE_ASSOCIATED_CLASS, None) + associated_class = cls._discover_associated_class(class_name, attrs, + inherited_associated_class) - # If inheriting the factory from a parent, keep a link to it. - # This allows to use the sequence counters from the parents. - if associated_class == inherited_associated_class: - attrs['_base_factory'] = base + # If inheriting the factory from a parent, keep a link to it. + # This allows to use the sequence counters from the parents. + if associated_class == inherited_associated_class: + attrs['_base_factory'] = base - # The CLASS_ATTRIBUTE_ASSOCIATED_CLASS must *not* be taken into account - # when parsing the declared attributes of the new class. - extra_attrs = {CLASS_ATTRIBUTE_ASSOCIATED_CLASS: associated_class} + # The CLASS_ATTRIBUTE_ASSOCIATED_CLASS must *not* be taken into account + # when parsing the declared attributes of the new class. + extra_attrs = {CLASS_ATTRIBUTE_ASSOCIATED_CLASS: associated_class} - return super(FactoryMetaClass, cls).__new__(cls, class_name, bases, attrs, extra_attrs=extra_attrs) + # Extract pre- and post-generation declarations + attributes = cls._extract_declarations(parent_factories, attrs) + + # Add extra args if provided. + if extra_attrs: + attributes.update(extra_attrs) + + return super(FactoryMetaClass, cls).__new__(cls, class_name, bases, attributes) def __str__(self): return '<%s for %s>' % (self.__name__, getattr(self, CLASS_ATTRIBUTE_ASSOCIATED_CLASS).__name__) + # Factory base classes class BaseFactory(object): @@ -430,10 +431,8 @@ class BaseFactory(object): return cls.generate_batch(strategy, size, **kwargs) -class StubFactory(BaseFactory): - __metaclass__ = BaseFactoryMetaClass - - default_strategy = STUB_STRATEGY +class AssociatedClassError(RuntimeError): + pass class Factory(BaseFactory): @@ -444,10 +443,8 @@ class Factory(BaseFactory): """ __metaclass__ = FactoryMetaClass - default_strategy = CREATE_STRATEGY - - class AssociatedClassError(RuntimeError): - pass + ABSTRACT_FACTORY = True + FACTORY_STRATEGY = CREATE_STRATEGY @classmethod def _adjust_kwargs(cls, **kwargs): @@ -522,6 +519,19 @@ class Factory(BaseFactory): return cls._generate(True, attrs) +Factory.AssociatedClassError = AssociatedClassError + + +class StubFactory(BaseFactory): + __metaclass__ = FactoryMetaClass + + FACTORY_STRATEGY = STUB_STRATEGY + + FACTORY_FOR = containers.StubObject + +print StubFactory._associated_class + + class DjangoModelFactory(Factory): """Factory for Django models. @@ -643,6 +653,6 @@ def use_strategy(new_strategy): This is an alternative to setting default_strategy in the class definition. """ def wrapped_class(klass): - klass.default_strategy = new_strategy + klass.FACTORY_STRATEGY = new_strategy return klass return wrapped_class -- cgit v1.2.3 From efc0ca41dcec074176064faf1e899ea275bb2901 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Fri, 15 Mar 2013 01:39:57 +0100 Subject: Fix exception hierarchy. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Raphaël Barrois --- factory/base.py | 31 ++++++++++++++++++++----------- 1 file changed, 20 insertions(+), 11 deletions(-) (limited to 'factory') diff --git a/factory/base.py b/factory/base.py index aef21d5..2418675 100644 --- a/factory/base.py +++ b/factory/base.py @@ -41,6 +41,22 @@ CLASS_ATTRIBUTE_POSTGEN_DECLARATIONS = '_postgen_declarations' CLASS_ATTRIBUTE_ASSOCIATED_CLASS = '_associated_class' +class FactoryError(Exception): + """Any exception raised by factory_boy.""" + + +class AssociatedClassError(FactoryError): + """Exception for Factory subclasses lacking FACTORY_FOR.""" + + +class UnknownStrategy(FactoryError): + """Raised when a factory uses an unknown strategy.""" + + +class UnsupportedStrategy(FactoryError): + """Raised when trying to use a strategy on an incompatible Factory.""" + + # Factory metaclasses def get_factory_bases(bases): @@ -64,7 +80,7 @@ class FactoryMetaClass(type): elif cls.FACTORY_STRATEGY == STUB_STRATEGY: return cls.stub(**kwargs) else: - raise BaseFactory.UnknownStrategy('Unknown FACTORY_STRATEGY: {0}'.format(cls.FACTORY_STRATEGY)) + raise UnknownStrategy('Unknown FACTORY_STRATEGY: {0}'.format(cls.FACTORY_STRATEGY)) @classmethod def _discover_associated_class(cls, class_name, attrs, inherited=None): @@ -200,15 +216,12 @@ class FactoryMetaClass(type): class BaseFactory(object): """Factory base support for sequences, attributes and stubs.""" - class UnknownStrategy(RuntimeError): - pass - - class UnsupportedStrategy(RuntimeError): - pass + UnknownStrategy = UnknownStrategy + UnsupportedStrategy = UnsupportedStrategy def __new__(cls, *args, **kwargs): """Would be called if trying to instantiate the class.""" - raise RuntimeError('You cannot instantiate BaseFactory') + raise FactoryError('You cannot instantiate BaseFactory') # ID to use for the next 'declarations.Sequence' attribute. _next_sequence = None @@ -431,10 +444,6 @@ class BaseFactory(object): return cls.generate_batch(strategy, size, **kwargs) -class AssociatedClassError(RuntimeError): - pass - - class Factory(BaseFactory): """Factory base with build and create support. -- cgit v1.2.3 From 66e4f2eadeae535e10f2318d4d4ed7041337ce5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Fri, 15 Mar 2013 01:43:07 +0100 Subject: Merge Factory into BaseFactory. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Raphaël Barrois --- factory/base.py | 148 +++++++++++++++++++++++++++----------------------------- 1 file changed, 72 insertions(+), 76 deletions(-) (limited to 'factory') diff --git a/factory/base.py b/factory/base.py index 2418675..17bd267 100644 --- a/factory/base.py +++ b/factory/base.py @@ -289,6 +289,68 @@ class BaseFactory(object): """ return getattr(cls, CLASS_ATTRIBUTE_DECLARATIONS).copy(extra_defs) + @classmethod + def _adjust_kwargs(cls, **kwargs): + """Extension point for custom kwargs adjustment.""" + return kwargs + + @classmethod + def _prepare(cls, create, **kwargs): + """Prepare an object for this factory. + + Args: + create: bool, whether to create or to build the object + **kwargs: arguments to pass to the creation function + """ + target_class = getattr(cls, CLASS_ATTRIBUTE_ASSOCIATED_CLASS) + kwargs = cls._adjust_kwargs(**kwargs) + + # Extract *args from **kwargs + args = tuple(kwargs.pop(key) for key in cls.FACTORY_ARG_PARAMETERS) + + if create: + return cls._create(target_class, *args, **kwargs) + else: + return cls._build(target_class, *args, **kwargs) + + @classmethod + def _generate(cls, create, attrs): + """generate the object. + + Args: + create (bool): whether to 'build' or 'create' the object + attrs (dict): attributes to use for generating the object + """ + # Extract declarations used for post-generation + postgen_declarations = getattr(cls, CLASS_ATTRIBUTE_POSTGEN_DECLARATIONS) + postgen_attributes = {} + for name, decl in sorted(postgen_declarations.items()): + postgen_attributes[name] = decl.extract(name, attrs) + + # Generate the object + obj = cls._prepare(create, **attrs) + + # Handle post-generation attributes + results = {} + for name, decl in sorted(postgen_declarations.items()): + extracted, extracted_kwargs = postgen_attributes[name] + results[name] = decl.call(obj, create, extracted, **extracted_kwargs) + + cls._after_postgeneration(obj, create, results) + + return obj + + @classmethod + def _after_postgeneration(cls, obj, create, results=None): + """Hook called after post-generation declarations have been handled. + + Args: + obj (object): the generated object + create (bool): whether the strategy was 'build' or 'create' + results (dict or None): result of post-generation declarations + """ + pass + @classmethod def _build(cls, target_class, *args, **kwargs): """Actually build an instance of the target_class. @@ -322,7 +384,8 @@ class BaseFactory(object): @classmethod def build(cls, **kwargs): """Build an instance of the associated class, with overriden attrs.""" - raise cls.UnsupportedStrategy() + attrs = cls.attributes(create=False, extra=kwargs) + return cls._generate(False, attrs) @classmethod def build_batch(cls, size, **kwargs): @@ -339,7 +402,8 @@ class BaseFactory(object): @classmethod def create(cls, **kwargs): """Create an instance of the associated class, with overriden attrs.""" - raise cls.UnsupportedStrategy() + attrs = cls.attributes(create=True, extra=kwargs) + return cls._generate(True, attrs) @classmethod def create_batch(cls, size, **kwargs): @@ -455,90 +519,22 @@ class Factory(BaseFactory): ABSTRACT_FACTORY = True FACTORY_STRATEGY = CREATE_STRATEGY - @classmethod - def _adjust_kwargs(cls, **kwargs): - """Extension point for custom kwargs adjustment.""" - return kwargs - @classmethod - def _prepare(cls, create, **kwargs): - """Prepare an object for this factory. - - Args: - create: bool, whether to create or to build the object - **kwargs: arguments to pass to the creation function - """ - target_class = getattr(cls, CLASS_ATTRIBUTE_ASSOCIATED_CLASS) - kwargs = cls._adjust_kwargs(**kwargs) - - # Extract *args from **kwargs - args = tuple(kwargs.pop(key) for key in cls.FACTORY_ARG_PARAMETERS) - - if create: - return cls._create(target_class, *args, **kwargs) - else: - return cls._build(target_class, *args, **kwargs) - - @classmethod - def _generate(cls, create, attrs): - """generate the object. - - Args: - create (bool): whether to 'build' or 'create' the object - attrs (dict): attributes to use for generating the object - """ - # Extract declarations used for post-generation - postgen_declarations = getattr(cls, CLASS_ATTRIBUTE_POSTGEN_DECLARATIONS) - postgen_attributes = {} - for name, decl in sorted(postgen_declarations.items()): - postgen_attributes[name] = decl.extract(name, attrs) - - # Generate the object - obj = cls._prepare(create, **attrs) - - # Handle post-generation attributes - results = {} - for name, decl in sorted(postgen_declarations.items()): - extracted, extracted_kwargs = postgen_attributes[name] - results[name] = decl.call(obj, create, extracted, **extracted_kwargs) - - cls._after_postgeneration(obj, create, results) +Factory.AssociatedClassError = AssociatedClassError - return obj - @classmethod - def _after_postgeneration(cls, obj, create, results=None): - """Hook called after post-generation declarations have been handled. +class StubFactory(Factory): - Args: - obj (object): the generated object - create (bool): whether the strategy was 'build' or 'create' - results (dict or None): result of post-generation declarations - """ - pass + FACTORY_STRATEGY = STUB_STRATEGY + FACTORY_FOR = containers.StubObject @classmethod def build(cls, **kwargs): - attrs = cls.attributes(create=False, extra=kwargs) - return cls._generate(False, attrs) + raise UnsupportedStrategy() @classmethod def create(cls, **kwargs): - attrs = cls.attributes(create=True, extra=kwargs) - return cls._generate(True, attrs) - - -Factory.AssociatedClassError = AssociatedClassError - - -class StubFactory(BaseFactory): - __metaclass__ = FactoryMetaClass - - FACTORY_STRATEGY = STUB_STRATEGY - - FACTORY_FOR = containers.StubObject - -print StubFactory._associated_class + raise UnsupportedStrategy() class DjangoModelFactory(Factory): -- cgit v1.2.3 From 624aedf03974bedb34349d0664fb863935e99969 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Fri, 15 Mar 2013 01:45:27 +0100 Subject: Make the Factory class Py3 compatible. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Raphaël Barrois --- factory/base.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) (limited to 'factory') diff --git a/factory/base.py b/factory/base.py index 17bd267..f3d5eab 100644 --- a/factory/base.py +++ b/factory/base.py @@ -508,18 +508,18 @@ class BaseFactory(object): return cls.generate_batch(strategy, size, **kwargs) -class Factory(BaseFactory): - """Factory base with build and create support. +Factory = FactoryMetaClass('Factory', (BaseFactory,), { + 'ABSTRACT_FACTORY': True, + 'FACTORY_STRATEGY': CREATE_STRATEGY, + '__doc__': """Factory base with build and create support. This class has the ability to support multiple ORMs by using custom creation functions. - """ - __metaclass__ = FactoryMetaClass - - ABSTRACT_FACTORY = True - FACTORY_STRATEGY = CREATE_STRATEGY + """, + }) +# Backwards compatibility Factory.AssociatedClassError = AssociatedClassError -- cgit v1.2.3 From 6c4f5846c8e21d6e48347b7e661edb72ffabb9f1 Mon Sep 17 00:00:00 2001 From: nkryptic Date: Tue, 12 Mar 2013 01:08:59 -0400 Subject: Add full Python 3 compatibility (Closes #10, #20, #49). MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Also: - update travis.yml to build against 2.6-2.7 and 3.2-3.3 - Switch to relative imports Signed-off-by: Raphaël Barrois --- factory/base.py | 2 +- factory/compat.py | 33 +++++++++++++++++++++++++++++++++ factory/containers.py | 4 ++-- factory/declarations.py | 7 ++++--- factory/utils.py | 3 ++- 5 files changed, 42 insertions(+), 7 deletions(-) create mode 100644 factory/compat.py (limited to 'factory') diff --git a/factory/base.py b/factory/base.py index f3d5eab..ff3e558 100644 --- a/factory/base.py +++ b/factory/base.py @@ -24,7 +24,7 @@ import re import sys import warnings -from factory import containers +from . import containers # Strategies BUILD_STRATEGY = 'build' diff --git a/factory/compat.py b/factory/compat.py new file mode 100644 index 0000000..a924de0 --- /dev/null +++ b/factory/compat.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2010 Mark Sandstrom +# Copyright (c) 2011-2013 Raphaël Barrois +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + + +"""Compatibility tools""" + +import sys + +is_python2 = (sys.version_info[0] == 2) + +if is_python2: + string_types = (str, unicode) +else: + string_types = (str,) diff --git a/factory/containers.py b/factory/containers.py index 31ee58b..0859a10 100644 --- a/factory/containers.py +++ b/factory/containers.py @@ -21,8 +21,8 @@ # THE SOFTWARE. -from factory import declarations -from factory import utils +from . import declarations +from . import utils class CyclicDefinitionError(Exception): diff --git a/factory/declarations.py b/factory/declarations.py index b491bfb..2b1fc05 100644 --- a/factory/declarations.py +++ b/factory/declarations.py @@ -25,7 +25,8 @@ import collections import itertools import warnings -from factory import utils +from . import compat +from . import utils class OrderedDeclaration(object): @@ -294,7 +295,7 @@ class SubFactory(ParameteredAttribute): self.factory_module = self.factory_name = '' else: # Must be a string - if not isinstance(factory, basestring) or '.' not in factory: + if not isinstance(factory, compat.string_types) or '.' not in factory: raise ValueError( "The argument of a SubFactory must be either a class " "or the fully qualified path to a Factory class; got " @@ -393,7 +394,7 @@ class RelatedFactory(PostGenerationDeclaration): self.factory_module = self.factory_name = '' else: # Must be a string - if not isinstance(factory, basestring) or '.' not in factory: + if not isinstance(factory, compat.string_types) or '.' not in factory: raise ValueError( "The argument of a SubFactory must be either a class " "or the fully qualified path to a Factory class; got " diff --git a/factory/utils.py b/factory/utils.py index 90fdfc3..fb8cfef 100644 --- a/factory/utils.py +++ b/factory/utils.py @@ -43,7 +43,8 @@ def extract_dict(prefix, kwargs, pop=True, exclude=()): """ prefix = prefix + ATTR_SPLITTER extracted = {} - for key in kwargs.keys(): + + for key in list(kwargs): if key in exclude: continue -- cgit v1.2.3 From 4d6847eb93aa269130ad803fb50be99c4a7508d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Sun, 24 Mar 2013 20:02:08 +0100 Subject: Default Sequence.type to int (Closes #50). --- factory/declarations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'factory') diff --git a/factory/declarations.py b/factory/declarations.py index 2b1fc05..a284930 100644 --- a/factory/declarations.py +++ b/factory/declarations.py @@ -168,7 +168,7 @@ class Sequence(OrderedDeclaration): type (function): A function converting an integer into the expected kind of counter for the 'function' attribute. """ - def __init__(self, function, type=str): + def __init__(self, function, type=int): super(Sequence, self).__init__() self.function = function self.type = type -- cgit v1.2.3 From 54a915fa25a66d3b7732a096eba7c2dd4a7b5a8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Tue, 2 Apr 2013 22:51:15 +0200 Subject: declarations: minor code simplification --- factory/declarations.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) (limited to 'factory') diff --git a/factory/declarations.py b/factory/declarations.py index a284930..2122bd2 100644 --- a/factory/declarations.py +++ b/factory/declarations.py @@ -320,10 +320,7 @@ class SubFactory(ParameteredAttribute): override the wrapped factory's defaults """ subfactory = self.get_factory() - if create: - return subfactory.create(**params) - else: - return subfactory.build(**params) + return subfactory.simple_generate(create, **params) class PostGenerationDeclaration(object): -- cgit v1.2.3 From 69e7a86875f97dc12e941302fabe417122f2cb7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Tue, 2 Apr 2013 23:07:03 +0200 Subject: Add Factory.FACTORY_HIDDEN_ARGS. Fields listed in this class attributes will be removed from the kwargs dict passed to the associated class for building/creation. --- factory/base.py | 7 +++++++ 1 file changed, 7 insertions(+) (limited to 'factory') diff --git a/factory/base.py b/factory/base.py index ff3e558..58cd50b 100644 --- a/factory/base.py +++ b/factory/base.py @@ -234,6 +234,9 @@ class BaseFactory(object): # List of arguments that should be passed as *args instead of **kwargs FACTORY_ARG_PARAMETERS = () + # List of attributes that should not be passed to the underlying class + FACTORY_HIDDEN_ARGS = () + @classmethod def _setup_next_sequence(cls): """Set up an initial sequence value for Sequence attributes. @@ -305,6 +308,10 @@ class BaseFactory(object): target_class = getattr(cls, CLASS_ATTRIBUTE_ASSOCIATED_CLASS) kwargs = cls._adjust_kwargs(**kwargs) + # Remove 'hidden' arguments. + for arg in cls.FACTORY_HIDDEN_ARGS: + del kwargs[arg] + # Extract *args from **kwargs args = tuple(kwargs.pop(key) for key in cls.FACTORY_ARG_PARAMETERS) -- cgit v1.2.3 From 6532f25058a13e81b1365bb353848510821f571f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Tue, 2 Apr 2013 23:34:28 +0200 Subject: Add support for get_or_create in DjangoModelFactory. --- factory/base.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) (limited to 'factory') diff --git a/factory/base.py b/factory/base.py index 58cd50b..71c2eb1 100644 --- a/factory/base.py +++ b/factory/base.py @@ -554,6 +554,7 @@ class DjangoModelFactory(Factory): """ ABSTRACT_FACTORY = True + FACTORY_DJANGO_GET_OR_CREATE = () @classmethod def _get_manager(cls, target_class): @@ -578,7 +579,19 @@ class DjangoModelFactory(Factory): def _create(cls, target_class, *args, **kwargs): """Create an instance of the model, and save it to the database.""" manager = cls._get_manager(target_class) - return manager.create(*args, **kwargs) + + assert 'defaults' not in cls.FACTORY_DJANGO_GET_OR_CREATE, ( + "'defaults' is a reserved keyword for get_or_create " + "(in %s.FACTORY_DJANGO_GET_OR_CREATE=%r)" + % (cls, cls.FACTORY_DJANGO_GET_OR_CREATE)) + + key_fields = {} + for field in cls.FACTORY_DJANGO_GET_OR_CREATE: + key_fields[field] = kwargs.pop(field) + key_fields['defaults'] = kwargs + + obj, created = manager.get_or_create(*args, **key_fields) + return obj @classmethod def _after_postgeneration(cls, obj, create, results=None): -- cgit v1.2.3 From 4c0d0650610e154499511d27268ed7c1d32b60db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Wed, 3 Apr 2013 00:42:05 +0200 Subject: internal: merge OrderedDeclaration.evaluate() variants. --- factory/containers.py | 51 ++++++++++++++++++------------------------------- factory/declarations.py | 39 +++++++++++++++++++++++++++++-------- 2 files changed, 50 insertions(+), 40 deletions(-) (limited to 'factory') diff --git a/factory/containers.py b/factory/containers.py index 0859a10..dc3a457 100644 --- a/factory/containers.py +++ b/factory/containers.py @@ -172,30 +172,6 @@ class LazyValue(object): raise NotImplementedError("This is an abstract method.") -class SubFactoryWrapper(LazyValue): - """Lazy wrapper around a SubFactory. - - Attributes: - subfactory (declarations.SubFactory): the SubFactory being wrapped - subfields (DeclarationDict): Default values to override when evaluating - the SubFactory - create (bool): whether to 'create' or 'build' the SubFactory. - """ - - def __init__(self, subfactory, subfields, create, *args, **kwargs): - super(SubFactoryWrapper, self).__init__(*args, **kwargs) - self.subfactory = subfactory - self.subfields = subfields - self.create = create - - def evaluate(self, obj, containers=()): - expanded_containers = (obj,) - if containers: - expanded_containers += tuple(containers) - return self.subfactory.evaluate(self.create, self.subfields, - expanded_containers) - - class OrderedDeclarationWrapper(LazyValue): """Lazy wrapper around an OrderedDeclaration. @@ -206,10 +182,12 @@ class OrderedDeclarationWrapper(LazyValue): declaration """ - def __init__(self, declaration, sequence, *args, **kwargs): - super(OrderedDeclarationWrapper, self).__init__(*args, **kwargs) + def __init__(self, declaration, sequence, create, extra=None, **kwargs): + super(OrderedDeclarationWrapper, self).__init__(**kwargs) self.declaration = declaration self.sequence = sequence + self.create = create + self.extra = extra def evaluate(self, obj, containers=()): """Lazily evaluate the attached OrderedDeclaration. @@ -219,7 +197,14 @@ class OrderedDeclarationWrapper(LazyValue): containers (object list): the chain of containers of the object being built, its immediate holder being first. """ - return self.declaration.evaluate(self.sequence, obj, containers) + return self.declaration.evaluate(self.sequence, obj, + create=self.create, + extra=self.extra, + containers=containers, + ) + + def __repr__(self): + return '<%s for %r>' % (self.__class__.__name__, self.declaration) class AttributeBuilder(object): @@ -240,7 +225,7 @@ class AttributeBuilder(object): extra = {} self.factory = factory - self._containers = extra.pop('__containers', None) + self._containers = extra.pop('__containers', ()) self._attrs = factory.declarations(extra) attrs_with_subfields = [k for k, v in self._attrs.items() if self.has_subfields(v)] @@ -263,10 +248,12 @@ class AttributeBuilder(object): # OrderedDeclaration. wrapped_attrs = {} for k, v in self._attrs.items(): - if isinstance(v, declarations.SubFactory): - v = SubFactoryWrapper(v, self._subfields.get(k, {}), create) - elif isinstance(v, declarations.OrderedDeclaration): - v = OrderedDeclarationWrapper(v, self.factory.sequence) + if isinstance(v, declarations.OrderedDeclaration): + v = OrderedDeclarationWrapper(v, + sequence=self.factory.sequence, + create=create, + extra=self._subfields.get(k, {}), + ) wrapped_attrs[k] = v stub = LazyStub(wrapped_attrs, containers=self._containers, diff --git a/factory/declarations.py b/factory/declarations.py index 2122bd2..15d8d5b 100644 --- a/factory/declarations.py +++ b/factory/declarations.py @@ -37,7 +37,7 @@ class OrderedDeclaration(object): in the same factory. """ - def evaluate(self, sequence, obj, containers=()): + def evaluate(self, sequence, obj, create, extra=None, containers=()): """Evaluate this declaration. Args: @@ -47,6 +47,10 @@ class OrderedDeclaration(object): attributes containers (list of containers.LazyStub): The chain of SubFactory which led to building this object. + create (bool): whether the target class should be 'built' or + 'created' + extra (DeclarationDict or None): extracted key/value extracted from + the attribute prefix """ raise NotImplementedError('This is an abstract method') @@ -63,7 +67,7 @@ class LazyAttribute(OrderedDeclaration): super(LazyAttribute, self).__init__(*args, **kwargs) self.function = function - def evaluate(self, sequence, obj, containers=()): + def evaluate(self, sequence, obj, create, extra=None, containers=()): return self.function(obj) @@ -122,7 +126,7 @@ class SelfAttribute(OrderedDeclaration): self.attribute_name = attribute_name self.default = default - def evaluate(self, sequence, obj, containers=()): + def evaluate(self, sequence, obj, create, extra=None, containers=()): if self.depth > 1: # Fetching from a parent target = containers[self.depth - 2] @@ -130,6 +134,13 @@ class SelfAttribute(OrderedDeclaration): target = obj return deepgetattr(target, self.attribute_name, self.default) + def __repr__(self): + return '<%s(%r, default=%r)>' % ( + self.__class__.__name__, + self.attribute_name, + self.default, + ) + class Iterator(OrderedDeclaration): """Fill this value using the values returned by an iterator. @@ -150,7 +161,7 @@ class Iterator(OrderedDeclaration): else: self.iterator = iter(iterator) - def evaluate(self, sequence, obj, containers=()): + def evaluate(self, sequence, obj, create, extra=None, containers=()): value = next(self.iterator) if self.getter is None: return value @@ -173,7 +184,7 @@ class Sequence(OrderedDeclaration): self.function = function self.type = type - def evaluate(self, sequence, obj, containers=()): + def evaluate(self, sequence, obj, create, extra=None, containers=()): return self.function(self.type(sequence)) @@ -186,7 +197,7 @@ class LazyAttributeSequence(Sequence): type (function): A function converting an integer into the expected kind of counter for the 'function' attribute. """ - def evaluate(self, sequence, obj, containers=()): + def evaluate(self, sequence, obj, create, extra=None, containers=()): return self.function(obj, self.type(sequence)) @@ -204,7 +215,7 @@ class ContainerAttribute(OrderedDeclaration): self.function = function self.strict = strict - def evaluate(self, sequence, obj, containers=()): + def evaluate(self, sequence, obj, create, extra=None, containers=()): """Evaluate the current ContainerAttribute. Args: @@ -237,11 +248,20 @@ class ParameteredAttribute(OrderedDeclaration): CONTAINERS_FIELD = '__containers' + # Whether to add the current object to the stack of containers + EXTEND_CONTAINERS = False + def __init__(self, **kwargs): super(ParameteredAttribute, self).__init__() self.defaults = kwargs - def evaluate(self, create, extra, containers): + def _prepare_containers(self, obj, containers=()): + if self.EXTEND_CONTAINERS: + return (obj,) + tuple(containers) + + return containers + + def evaluate(self, sequence, obj, create, extra=None, containers=()): """Evaluate the current definition and fill its attributes. Uses attributes definition in the following order: @@ -260,6 +280,7 @@ class ParameteredAttribute(OrderedDeclaration): if extra: defaults.update(extra) if self.CONTAINERS_FIELD: + containers = self._prepare_containers(obj, containers) defaults[self.CONTAINERS_FIELD] = containers return self.generate(create, defaults) @@ -288,6 +309,8 @@ class SubFactory(ParameteredAttribute): factory (base.Factory): the wrapped factory """ + EXTEND_CONTAINERS = True + def __init__(self, factory, **kwargs): super(SubFactory, self).__init__(**kwargs) if isinstance(factory, type): -- cgit v1.2.3 From 8c1784e8c1eac65f66b4a1ecc4b8b0ddd5de9327 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Wed, 3 Apr 2013 01:17:26 +0200 Subject: Pylint. --- factory/__init__.py | 28 ++++++----- factory/base.py | 123 +++++++++++++----------------------------------- factory/compat.py | 10 ++-- factory/containers.py | 7 ++- factory/declarations.py | 39 ++++----------- factory/helpers.py | 123 ++++++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 190 insertions(+), 140 deletions(-) create mode 100644 factory/helpers.py (limited to 'factory') diff --git a/factory/__init__.py b/factory/__init__.py index adcf9c9..beb422e 100644 --- a/factory/__init__.py +++ b/factory/__init__.py @@ -28,19 +28,6 @@ from .base import ( StubFactory, DjangoModelFactory, - build, - create, - stub, - generate, - simple_generate, - make_factory, - - build_batch, - create_batch, - stub_batch, - generate_batch, - simple_generate_batch, - BUILD_STRATEGY, CREATE_STRATEGY, STUB_STRATEGY, @@ -58,6 +45,21 @@ from .declarations import ( PostGeneration, PostGenerationMethodCall, RelatedFactory, +) + +from .helpers import ( + build, + create, + stub, + generate, + simple_generate, + make_factory, + + build_batch, + create_batch, + stub_batch, + generate_batch, + simple_generate_batch, lazy_attribute, iterator, diff --git a/factory/base.py b/factory/base.py index 71c2eb1..13a0623 100644 --- a/factory/base.py +++ b/factory/base.py @@ -20,10 +20,6 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. -import re -import sys -import warnings - from . import containers # Strategies @@ -68,7 +64,7 @@ class FactoryMetaClass(type): """Factory metaclass for handling ordered declarations.""" def __call__(cls, **kwargs): - """Override the default Factory() syntax to call the default build strategy. + """Override the default Factory() syntax to call the default strategy. Returns an instance of the associated class. """ @@ -80,10 +76,11 @@ class FactoryMetaClass(type): elif cls.FACTORY_STRATEGY == STUB_STRATEGY: return cls.stub(**kwargs) else: - raise UnknownStrategy('Unknown FACTORY_STRATEGY: {0}'.format(cls.FACTORY_STRATEGY)) + raise UnknownStrategy('Unknown FACTORY_STRATEGY: {0}'.format( + cls.FACTORY_STRATEGY)) @classmethod - def _discover_associated_class(cls, class_name, attrs, inherited=None): + def _discover_associated_class(mcs, class_name, attrs, inherited=None): """Try to find the class associated with this factory. In order, the following tests will be performed: @@ -104,8 +101,6 @@ class FactoryMetaClass(type): AssociatedClassError: If we were unable to associate this factory to a class. """ - own_associated_class = None - if FACTORY_CLASS_DECLARATION in attrs: return attrs[FACTORY_CLASS_DECLARATION] @@ -120,7 +115,7 @@ class FactoryMetaClass(type): class_name) @classmethod - def _extract_declarations(cls, bases, attributes): + def _extract_declarations(mcs, bases, attributes): """Extract declarations from a class definition. Args: @@ -141,7 +136,8 @@ class FactoryMetaClass(type): postgen_declarations.update_with_public( getattr(base, CLASS_ATTRIBUTE_POSTGEN_DECLARATIONS, {})) # Import all 'public' attributes (avoid those starting with _) - declarations.update_with_public(getattr(base, CLASS_ATTRIBUTE_DECLARATIONS, {})) + declarations.update_with_public( + getattr(base, CLASS_ATTRIBUTE_DECLARATIONS, {})) # Import attributes from the class definition attributes = postgen_declarations.update_with_public(attributes) @@ -154,7 +150,7 @@ class FactoryMetaClass(type): return attributes - def __new__(cls, class_name, bases, attrs, extra_attrs=None): + def __new__(mcs, class_name, bases, attrs, extra_attrs=None): """Record attributes as a pattern for later instance construction. This is called when a new Factory subclass is defined; it will collect @@ -174,7 +170,8 @@ class FactoryMetaClass(type): """ parent_factories = get_factory_bases(bases) if not parent_factories: - return super(FactoryMetaClass, cls).__new__(cls, class_name, bases, attrs) + return super(FactoryMetaClass, mcs).__new__( + mcs, class_name, bases, attrs) is_abstract = attrs.pop('ABSTRACT_FACTORY', False) extra_attrs = {} @@ -185,7 +182,7 @@ class FactoryMetaClass(type): inherited_associated_class = getattr(base, CLASS_ATTRIBUTE_ASSOCIATED_CLASS, None) - associated_class = cls._discover_associated_class(class_name, attrs, + associated_class = mcs._discover_associated_class(class_name, attrs, inherited_associated_class) # If inheriting the factory from a parent, keep a link to it. @@ -193,22 +190,23 @@ class FactoryMetaClass(type): if associated_class == inherited_associated_class: attrs['_base_factory'] = base - # The CLASS_ATTRIBUTE_ASSOCIATED_CLASS must *not* be taken into account - # when parsing the declared attributes of the new class. + # The CLASS_ATTRIBUTE_ASSOCIATED_CLASS must *not* be taken into + # account when parsing the declared attributes of the new class. extra_attrs = {CLASS_ATTRIBUTE_ASSOCIATED_CLASS: associated_class} # Extract pre- and post-generation declarations - attributes = cls._extract_declarations(parent_factories, attrs) + attributes = mcs._extract_declarations(parent_factories, attrs) # Add extra args if provided. if extra_attrs: attributes.update(extra_attrs) - return super(FactoryMetaClass, cls).__new__(cls, class_name, bases, attributes) + return super(FactoryMetaClass, mcs).__new__( + mcs, class_name, bases, attributes) - def __str__(self): - return '<%s for %s>' % (self.__name__, - getattr(self, CLASS_ATTRIBUTE_ASSOCIATED_CLASS).__name__) + def __str__(cls): + return '<%s for %s>' % (cls.__name__, + getattr(cls, CLASS_ATTRIBUTE_ASSOCIATED_CLASS).__name__) # Factory base classes @@ -216,6 +214,7 @@ class FactoryMetaClass(type): class BaseFactory(object): """Factory base support for sequences, attributes and stubs.""" + # Backwards compatibility UnknownStrategy = UnknownStrategy UnsupportedStrategy = UnsupportedStrategy @@ -231,6 +230,9 @@ class BaseFactory(object): # class. _base_factory = None + # Holds the target class, once resolved. + _associated_class = None + # List of arguments that should be passed as *args instead of **kwargs FACTORY_ARG_PARAMETERS = () @@ -329,7 +331,8 @@ class BaseFactory(object): attrs (dict): attributes to use for generating the object """ # Extract declarations used for post-generation - postgen_declarations = getattr(cls, CLASS_ATTRIBUTE_POSTGEN_DECLARATIONS) + postgen_declarations = getattr(cls, + CLASS_ATTRIBUTE_POSTGEN_DECLARATIONS) postgen_attributes = {} for name, decl in sorted(postgen_declarations.items()): postgen_attributes[name] = decl.extract(name, attrs) @@ -341,7 +344,8 @@ class BaseFactory(object): results = {} for name, decl in sorted(postgen_declarations.items()): extracted, extracted_kwargs = postgen_attributes[name] - results[name] = decl.call(obj, create, extracted, **extracted_kwargs) + results[name] = decl.call(obj, create, extracted, + **extracted_kwargs) cls._after_postgeneration(obj, create, results) @@ -527,7 +531,7 @@ Factory = FactoryMetaClass('Factory', (BaseFactory,), { # Backwards compatibility -Factory.AssociatedClassError = AssociatedClassError +Factory.AssociatedClassError = AssociatedClassError # pylint: disable=W0201 class StubFactory(Factory): @@ -547,7 +551,7 @@ class StubFactory(Factory): class DjangoModelFactory(Factory): """Factory for Django models. - This makes sure that the 'sequence' field of created objects is an unused id. + This makes sure that the 'sequence' field of created objects is a new id. Possible improvement: define a new 'attribute' type, AutoField, which would handle those for non-numerical primary keys. @@ -559,7 +563,7 @@ class DjangoModelFactory(Factory): @classmethod def _get_manager(cls, target_class): try: - return target_class._default_manager + return target_class._default_manager # pylint: disable=W0212 except AttributeError: return target_class.objects @@ -567,7 +571,8 @@ class DjangoModelFactory(Factory): def _setup_next_sequence(cls): """Compute the next available PK, based on the 'pk' database field.""" - manager = cls._get_manager(cls._associated_class) + model = cls._associated_class # pylint: disable=E1101 + manager = cls._get_manager(model) try: return 1 + manager.values_list('pk', flat=True @@ -590,7 +595,7 @@ class DjangoModelFactory(Factory): key_fields[field] = kwargs.pop(field) key_fields['defaults'] = kwargs - obj, created = manager.get_or_create(*args, **key_fields) + obj, _created = manager.get_or_create(*args, **key_fields) return obj @classmethod @@ -610,68 +615,6 @@ class MogoFactory(Factory): return target_class.new(*args, **kwargs) -def make_factory(klass, **kwargs): - """Create a new, simple factory for the given class.""" - factory_name = '%sFactory' % klass.__name__ - kwargs[FACTORY_CLASS_DECLARATION] = klass - base_class = kwargs.pop('FACTORY_CLASS', Factory) - - factory_class = type(Factory).__new__(type(Factory), factory_name, (base_class,), kwargs) - factory_class.__name__ = '%sFactory' % klass.__name__ - factory_class.__doc__ = 'Auto-generated factory for class %s' % klass - return factory_class - - -def build(klass, **kwargs): - """Create a factory for the given class, and build an instance.""" - return make_factory(klass, **kwargs).build() - - -def build_batch(klass, size, **kwargs): - """Create a factory for the given class, and build a batch of instances.""" - return make_factory(klass, **kwargs).build_batch(size) - - -def create(klass, **kwargs): - """Create a factory for the given class, and create an instance.""" - return make_factory(klass, **kwargs).create() - - -def create_batch(klass, size, **kwargs): - """Create a factory for the given class, and create a batch of instances.""" - return make_factory(klass, **kwargs).create_batch(size) - - -def stub(klass, **kwargs): - """Create a factory for the given class, and stub an instance.""" - return make_factory(klass, **kwargs).stub() - - -def stub_batch(klass, size, **kwargs): - """Create a factory for the given class, and stub a batch of instances.""" - return make_factory(klass, **kwargs).stub_batch(size) - - -def generate(klass, strategy, **kwargs): - """Create a factory for the given class, and generate an instance.""" - return make_factory(klass, **kwargs).generate(strategy) - - -def generate_batch(klass, strategy, size, **kwargs): - """Create a factory for the given class, and generate instances.""" - return make_factory(klass, **kwargs).generate_batch(strategy, size) - - -def simple_generate(klass, create, **kwargs): - """Create a factory for the given class, and simple_generate an instance.""" - return make_factory(klass, **kwargs).simple_generate(create) - - -def simple_generate_batch(klass, create, size, **kwargs): - """Create a factory for the given class, and simple_generate instances.""" - return make_factory(klass, **kwargs).simple_generate_batch(create, size) - - def use_strategy(new_strategy): """Force the use of a different strategy. diff --git a/factory/compat.py b/factory/compat.py index a924de0..84f31b7 100644 --- a/factory/compat.py +++ b/factory/compat.py @@ -25,9 +25,11 @@ import sys -is_python2 = (sys.version_info[0] == 2) +PY2 = (sys.version_info[0] == 2) -if is_python2: - string_types = (str, unicode) +if PY2: + def is_string(obj): + return isinstance(obj, (str, unicode)) else: - string_types = (str,) + def is_string(obj): + return isinstance(obj, str) diff --git a/factory/containers.py b/factory/containers.py index dc3a457..e02f9f9 100644 --- a/factory/containers.py +++ b/factory/containers.py @@ -228,9 +228,12 @@ class AttributeBuilder(object): self._containers = extra.pop('__containers', ()) self._attrs = factory.declarations(extra) - attrs_with_subfields = [k for k, v in self._attrs.items() if self.has_subfields(v)] + attrs_with_subfields = [ + k for k, v in self._attrs.items() + if self.has_subfields(v)] - self._subfields = utils.multi_extract_dict(attrs_with_subfields, self._attrs) + self._subfields = utils.multi_extract_dict( + attrs_with_subfields, self._attrs) def has_subfields(self, value): return isinstance(value, declarations.SubFactory) diff --git a/factory/declarations.py b/factory/declarations.py index 15d8d5b..3d76960 100644 --- a/factory/declarations.py +++ b/factory/declarations.py @@ -21,9 +21,7 @@ # THE SOFTWARE. -import collections import itertools -import warnings from . import compat from . import utils @@ -179,7 +177,7 @@ class Sequence(OrderedDeclaration): type (function): A function converting an integer into the expected kind of counter for the 'function' attribute. """ - def __init__(self, function, type=int): + def __init__(self, function, type=int): # pylint: disable=W0622 super(Sequence, self).__init__() self.function = function self.type = type @@ -318,7 +316,7 @@ class SubFactory(ParameteredAttribute): self.factory_module = self.factory_name = '' else: # Must be a string - if not isinstance(factory, compat.string_types) or '.' not in factory: + if not (compat.is_string(factory) and '.' in factory): raise ValueError( "The argument of a SubFactory must be either a class " "or the fully qualified path to a Factory class; got " @@ -330,7 +328,8 @@ class SubFactory(ParameteredAttribute): """Retrieve the wrapped factory.Factory subclass.""" if self.factory is None: # Must be a module path - self.factory = utils.import_object(self.factory_module, self.factory_name) + self.factory = utils.import_object( + self.factory_module, self.factory_name) return self.factory def generate(self, create, params): @@ -390,10 +389,6 @@ class PostGeneration(PostGenerationDeclaration): return self.function(obj, create, extracted, **kwargs) -def post_generation(fun): - return PostGeneration(fun) - - class RelatedFactory(PostGenerationDeclaration): """Calls a factory once the object has been generated. @@ -414,7 +409,7 @@ class RelatedFactory(PostGenerationDeclaration): self.factory_module = self.factory_name = '' else: # Must be a string - if not isinstance(factory, compat.string_types) or '.' not in factory: + if not (compat.is_string(factory) and '.' in factory): raise ValueError( "The argument of a SubFactory must be either a class " "or the fully qualified path to a Factory class; got " @@ -426,7 +421,8 @@ class RelatedFactory(PostGenerationDeclaration): """Retrieve the wrapped factory.Factory subclass.""" if self.factory is None: # Must be a module path - self.factory = utils.import_object(self.factory_module, self.factory_name) + self.factory = utils.import_object( + self.factory_module, self.factory_name) return self.factory def call(self, obj, create, extracted=None, **kwargs): @@ -450,7 +446,7 @@ class PostGenerationMethodCall(PostGenerationDeclaration): Example: class UserFactory(factory.Factory): ... - password = factory.PostGenerationMethodCall('set_password', password='') + password = factory.PostGenerationMethodCall('set_pass', password='') """ def __init__(self, method_name, *args, **kwargs): super(PostGenerationMethodCall, self).__init__() @@ -472,22 +468,3 @@ class PostGenerationMethodCall(PostGenerationDeclaration): passed_kwargs.update(kwargs) method = getattr(obj, self.method_name) method(*passed_args, **passed_kwargs) - - -# Decorators... in case lambdas don't cut it - -def lazy_attribute(func): - return LazyAttribute(func) - -def iterator(func): - """Turn a generator function into an iterator attribute.""" - return Iterator(func()) - -def sequence(func): - return Sequence(func) - -def lazy_attribute_sequence(func): - return LazyAttributeSequence(func) - -def container_attribute(func): - return ContainerAttribute(func, strict=False) diff --git a/factory/helpers.py b/factory/helpers.py new file mode 100644 index 0000000..8f0d161 --- /dev/null +++ b/factory/helpers.py @@ -0,0 +1,123 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2010 Mark Sandstrom +# Copyright (c) 2011-2013 Raphaël Barrois +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + + +"""Simple wrappers around Factory class definition.""" + + +from . import base +from . import declarations + + +def make_factory(klass, **kwargs): + """Create a new, simple factory for the given class.""" + factory_name = '%sFactory' % klass.__name__ + kwargs[base.FACTORY_CLASS_DECLARATION] = klass + base_class = kwargs.pop('FACTORY_CLASS', base.Factory) + + factory_class = type(base.Factory).__new__( + type(base.Factory), factory_name, (base_class,), kwargs) + factory_class.__name__ = '%sFactory' % klass.__name__ + factory_class.__doc__ = 'Auto-generated factory for class %s' % klass + return factory_class + + +def build(klass, **kwargs): + """Create a factory for the given class, and build an instance.""" + return make_factory(klass, **kwargs).build() + + +def build_batch(klass, size, **kwargs): + """Create a factory for the given class, and build a batch of instances.""" + return make_factory(klass, **kwargs).build_batch(size) + + +def create(klass, **kwargs): + """Create a factory for the given class, and create an instance.""" + return make_factory(klass, **kwargs).create() + + +def create_batch(klass, size, **kwargs): + """Create a factory for the given class, and create a batch of instances.""" + return make_factory(klass, **kwargs).create_batch(size) + + +def stub(klass, **kwargs): + """Create a factory for the given class, and stub an instance.""" + return make_factory(klass, **kwargs).stub() + + +def stub_batch(klass, size, **kwargs): + """Create a factory for the given class, and stub a batch of instances.""" + return make_factory(klass, **kwargs).stub_batch(size) + + +def generate(klass, strategy, **kwargs): + """Create a factory for the given class, and generate an instance.""" + return make_factory(klass, **kwargs).generate(strategy) + + +def generate_batch(klass, strategy, size, **kwargs): + """Create a factory for the given class, and generate instances.""" + return make_factory(klass, **kwargs).generate_batch(strategy, size) + + +# We're reusing 'create' as a keyword. +# pylint: disable=W0621 + + +def simple_generate(klass, create, **kwargs): + """Create a factory for the given class, and simple_generate an instance.""" + return make_factory(klass, **kwargs).simple_generate(create) + + +def simple_generate_batch(klass, create, size, **kwargs): + """Create a factory for the given class, and simple_generate instances.""" + return make_factory(klass, **kwargs).simple_generate_batch(create, size) + + +# pylint: enable=W0621 + + +def lazy_attribute(func): + return declarations.LazyAttribute(func) + + +def iterator(func): + """Turn a generator function into an iterator attribute.""" + return declarations.Iterator(func()) + + +def sequence(func): + return declarations.Sequence(func) + + +def lazy_attribute_sequence(func): + return declarations.LazyAttributeSequence(func) + + +def container_attribute(func): + return declarations.ContainerAttribute(func, strict=False) + + +def post_generation(fun): + return declarations.PostGeneration(fun) -- cgit v1.2.3 From 54971381fb31167d1f47b5e705480e044d702602 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Thu, 11 Apr 2013 23:59:32 +0200 Subject: Add factory.fuzzy (Closes #41). --- factory/fuzzy.py | 86 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 factory/fuzzy.py (limited to 'factory') diff --git a/factory/fuzzy.py b/factory/fuzzy.py new file mode 100644 index 0000000..186b4a7 --- /dev/null +++ b/factory/fuzzy.py @@ -0,0 +1,86 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2010 Mark Sandstrom +# Copyright (c) 2011-2013 Raphaël Barrois +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + + +"""Additional declarations for "fuzzy" attribute definitions.""" + + +import random + +from . import declarations + + +class BaseFuzzyAttribute(declarations.OrderedDeclaration): + """Base class for fuzzy attributes. + + Custom fuzzers should override the `fuzz()` method. + """ + + def fuzz(self): + raise NotImplementedError() + + def evaluate(self, sequence, obj, create, extra=None, containers=()): + return self.fuzz() + + +class FuzzyAttribute(BaseFuzzyAttribute): + """Similar to LazyAttribute, but yields random values. + + Attributes: + function (callable): function taking no parameters and returning a + random value. + """ + + def __init__(self, fuzzer, **kwargs): + super(FuzzyAttribute, self).__init__(**kwargs) + self.fuzzer = fuzzer + + def fuzz(self): + return self.fuzzer() + + +class FuzzyChoice(BaseFuzzyAttribute): + """Handles fuzzy choice of an attribute.""" + + def __init__(self, choices, **kwargs): + self.choices = list(choices) + super(FuzzyChoice, self).__init__(**kwargs) + + def fuzz(self): + return random.choice(self.choices) + + +class FuzzyInteger(BaseFuzzyAttribute): + """Random integer within a given range.""" + + def __init__(self, low, high=None, **kwargs): + if high is None: + high = low + low = 0 + + self.low = low + self.high = high + + super(FuzzyInteger, self).__init__(**kwargs) + + def fuzz(self): + return random.randint(self.low, self.high) -- cgit v1.2.3 From e7a9a87320c78ec05a5d548516fe17c258e6d4c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Mon, 15 Apr 2013 02:21:08 +0200 Subject: Allow overriding the sequence counter. --- factory/base.py | 8 +++++++- factory/containers.py | 13 +++++++++---- 2 files changed, 16 insertions(+), 5 deletions(-) (limited to 'factory') diff --git a/factory/base.py b/factory/base.py index 13a0623..25f4714 100644 --- a/factory/base.py +++ b/factory/base.py @@ -282,7 +282,13 @@ class BaseFactory(object): applicable; the current list of computed attributes is available to the currently processed object. """ - return containers.AttributeBuilder(cls, extra).build(create) + force_sequence = None + if extra: + force_sequence = extra.pop('__sequence', None) + return containers.AttributeBuilder(cls, extra).build( + create=create, + force_sequence=force_sequence, + ) @classmethod def declarations(cls, extra_defs=None): diff --git a/factory/containers.py b/factory/containers.py index e02f9f9..ee2ad82 100644 --- a/factory/containers.py +++ b/factory/containers.py @@ -236,16 +236,21 @@ class AttributeBuilder(object): attrs_with_subfields, self._attrs) def has_subfields(self, value): - return isinstance(value, declarations.SubFactory) + return isinstance(value, declarations.ParameteredAttribute) - def build(self, create): + def build(self, create, force_sequence=None): """Build a dictionary of attributes. Args: create (bool): whether to 'build' or 'create' the subfactories. + force_sequence (int or None): if set to an int, use this value for + the sequence counter; don't advance the related counter. """ # Setup factory sequence. - self.factory.sequence = self.factory._generate_next_sequence() + if force_sequence is None: + sequence = self.factory._generate_next_sequence() + else: + sequence = force_sequence # Parse attribute declarations, wrapping SubFactory and # OrderedDeclaration. @@ -253,7 +258,7 @@ class AttributeBuilder(object): for k, v in self._attrs.items(): if isinstance(v, declarations.OrderedDeclaration): v = OrderedDeclarationWrapper(v, - sequence=self.factory.sequence, + sequence=sequence, create=create, extra=self._subfields.get(k, {}), ) -- cgit v1.2.3 From 2b661e6eae3187c05c4eb8e1c3790cee6a9e3032 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Mon, 15 Apr 2013 02:22:01 +0200 Subject: Add Dict/List declarations (Closes #18). --- factory/__init__.py | 7 +++++++ factory/base.py | 42 ++++++++++++++++++++++++++++++++++++++++++ factory/declarations.py | 35 ++++++++++++++++++++++++++++++++--- 3 files changed, 81 insertions(+), 3 deletions(-) (limited to 'factory') diff --git a/factory/__init__.py b/factory/__init__.py index beb422e..9843fa1 100644 --- a/factory/__init__.py +++ b/factory/__init__.py @@ -25,6 +25,11 @@ __author__ = 'Raphaël Barrois ' from .base import ( Factory, + BaseDictFactory, + DictFactory, + BaseListFactory, + ListFactory, + MogoFactory, StubFactory, DjangoModelFactory, @@ -42,6 +47,8 @@ from .declarations import ( SelfAttribute, ContainerAttribute, SubFactory, + Dict, + List, PostGeneration, PostGenerationMethodCall, RelatedFactory, diff --git a/factory/base.py b/factory/base.py index 25f4714..928ea7a 100644 --- a/factory/base.py +++ b/factory/base.py @@ -621,6 +621,48 @@ class MogoFactory(Factory): return target_class.new(*args, **kwargs) +class BaseDictFactory(Factory): + """Factory for dictionary-like classes.""" + ABSTRACT_FACTORY = True + + @classmethod + def _build(cls, target_class, *args, **kwargs): + if args: + raise ValueError( + "DictFactory %r does not support FACTORY_ARG_PARAMETERS.", cls) + return target_class(**kwargs) + + @classmethod + def _create(cls, target_class, *args, **kwargs): + return cls._build(target_class, *args, **kwargs) + + +class DictFactory(BaseDictFactory): + FACTORY_FOR = dict + + +class BaseListFactory(Factory): + """Factory for list-like classes.""" + ABSTRACT_FACTORY = True + + @classmethod + def _build(cls, target_class, *args, **kwargs): + if args: + raise ValueError( + "ListFactory %r does not support FACTORY_ARG_PARAMETERS.", cls) + + values = [v for k, v in sorted(kwargs.items())] + return target_class(values) + + @classmethod + def _create(cls, target_class, *args, **kwargs): + return cls._build(target_class, *args, **kwargs) + + +class ListFactory(BaseListFactory): + FACTORY_FOR = list + + def use_strategy(new_strategy): """Force the use of a different strategy. diff --git a/factory/declarations.py b/factory/declarations.py index 3d76960..974b4ac 100644 --- a/factory/declarations.py +++ b/factory/declarations.py @@ -281,12 +281,14 @@ class ParameteredAttribute(OrderedDeclaration): containers = self._prepare_containers(obj, containers) defaults[self.CONTAINERS_FIELD] = containers - return self.generate(create, defaults) + return self.generate(sequence, obj, create, defaults) - def generate(self, create, params): # pragma: no cover + def generate(self, sequence, obj, create, params): # pragma: no cover """Actually generate the related attribute. Args: + sequence (int): the current sequence number + obj (LazyStub): the object being constructed create (bool): whether the calling factory was in 'create' or 'build' mode params (dict): parameters inherited from init and evaluation-time @@ -332,7 +334,7 @@ class SubFactory(ParameteredAttribute): self.factory_module, self.factory_name) return self.factory - def generate(self, create, params): + def generate(self, sequence, obj, create, params): """Evaluate the current definition and fill its attributes. Args: @@ -345,6 +347,33 @@ class SubFactory(ParameteredAttribute): return subfactory.simple_generate(create, **params) +class Dict(SubFactory): + """Fill a dict with usual declarations.""" + + def __init__(self, params, dict_factory='factory.DictFactory'): + super(Dict, self).__init__(dict_factory, **dict(params)) + + def generate(self, sequence, obj, create, params): + dict_factory = self.get_factory() + return dict_factory.simple_generate(create, + __sequence=sequence, + **params) + + +class List(SubFactory): + """Fill a list with standard declarations.""" + + def __init__(self, params, list_factory='factory.ListFactory'): + params = dict((str(i), v) for i, v in enumerate(params)) + super(List, self).__init__(list_factory, **params) + + def generate(self, sequence, obj, create, params): + list_factory = self.get_factory() + return list_factory.simple_generate(create, + __sequence=sequence, + **params) + + class PostGenerationDeclaration(object): """Declarations to be called once the target object has been generated.""" -- cgit v1.2.3 From f35579bd37594b1d888e07db79bdd77a68f4f897 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Mon, 15 Apr 2013 23:22:19 +0200 Subject: Release v2.0.0 --- factory/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'factory') diff --git a/factory/__init__.py b/factory/__init__.py index 9843fa1..ef5d40e 100644 --- a/factory/__init__.py +++ b/factory/__init__.py @@ -20,7 +20,7 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. -__version__ = '2.0.0-dev' +__version__ = '2.0.0' __author__ = 'Raphaël Barrois ' from .base import ( -- cgit v1.2.3 From 68b5872e8cbd33f5f59ea8d859e326eb0ff0c6eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Tue, 16 Apr 2013 11:20:18 +0200 Subject: Release v2.0.1 --- factory/__init__.py | 2 +- factory/base.py | 11 +++++++---- 2 files changed, 8 insertions(+), 5 deletions(-) (limited to 'factory') diff --git a/factory/__init__.py b/factory/__init__.py index ef5d40e..939500c 100644 --- a/factory/__init__.py +++ b/factory/__init__.py @@ -20,7 +20,7 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. -__version__ = '2.0.0' +__version__ = '2.0.1' __author__ = 'Raphaël Barrois ' from .base import ( diff --git a/factory/base.py b/factory/base.py index 928ea7a..0d03838 100644 --- a/factory/base.py +++ b/factory/base.py @@ -596,10 +596,13 @@ class DjangoModelFactory(Factory): "(in %s.FACTORY_DJANGO_GET_OR_CREATE=%r)" % (cls, cls.FACTORY_DJANGO_GET_OR_CREATE)) - key_fields = {} - for field in cls.FACTORY_DJANGO_GET_OR_CREATE: - key_fields[field] = kwargs.pop(field) - key_fields['defaults'] = kwargs + if cls.FACTORY_DJANGO_GET_OR_CREATE: + key_fields = {} + for field in cls.FACTORY_DJANGO_GET_OR_CREATE: + key_fields[field] = kwargs.pop(field) + key_fields['defaults'] = kwargs + else: + key_fields = kwargs obj, _created = manager.get_or_create(*args, **key_fields) return obj -- cgit v1.2.3 From ef5011d7d28cc7034e07dc75a6661a0253c0fe1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Tue, 16 Apr 2013 11:32:36 +0200 Subject: Don't use objects.get_or_create() unless required. --- factory/base.py | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) (limited to 'factory') diff --git a/factory/base.py b/factory/base.py index 0d03838..2ff2944 100644 --- a/factory/base.py +++ b/factory/base.py @@ -587,8 +587,8 @@ class DjangoModelFactory(Factory): return 1 @classmethod - def _create(cls, target_class, *args, **kwargs): - """Create an instance of the model, and save it to the database.""" + def _get_or_create(cls, target_class, *args, **kwargs): + """Create an instance of the model through objects.get_or_create.""" manager = cls._get_manager(target_class) assert 'defaults' not in cls.FACTORY_DJANGO_GET_OR_CREATE, ( @@ -596,17 +596,24 @@ class DjangoModelFactory(Factory): "(in %s.FACTORY_DJANGO_GET_OR_CREATE=%r)" % (cls, cls.FACTORY_DJANGO_GET_OR_CREATE)) - if cls.FACTORY_DJANGO_GET_OR_CREATE: - key_fields = {} - for field in cls.FACTORY_DJANGO_GET_OR_CREATE: - key_fields[field] = kwargs.pop(field) - key_fields['defaults'] = kwargs - else: - key_fields = kwargs + key_fields = {} + for field in cls.FACTORY_DJANGO_GET_OR_CREATE: + key_fields[field] = kwargs.pop(field) + key_fields['defaults'] = kwargs obj, _created = manager.get_or_create(*args, **key_fields) return obj + @classmethod + def _create(cls, target_class, *args, **kwargs): + """Create an instance of the model, and save it to the database.""" + manager = cls._get_manager(target_class) + + if cls.FACTORY_DJANGO_GET_OR_CREATE: + return cls._get_or_create(target_class, *args, **kwargs) + + return manager.create(*args, **kwargs) + @classmethod def _after_postgeneration(cls, obj, create, results=None): """Save again the instance if creating and at least one hook ran.""" -- cgit v1.2.3 From 876845102c4a217496d0f6435bfe1e3726d31fe4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Barrois?= Date: Tue, 16 Apr 2013 11:33:00 +0200 Subject: Release v2.0.2 --- factory/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'factory') diff --git a/factory/__init__.py b/factory/__init__.py index 939500c..e1138fa 100644 --- a/factory/__init__.py +++ b/factory/__init__.py @@ -20,7 +20,7 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. -__version__ = '2.0.1' +__version__ = '2.0.2' __author__ = 'Raphaël Barrois ' from .base import ( -- cgit v1.2.3 From 8e5ee2fb19058336afb5af61486e17f2603b56cb Mon Sep 17 00:00:00 2001 From: Thomas Goirand Date: Sun, 12 May 2013 05:38:51 +0000 Subject: Fixed differences with upstream branch. --- factory/__init__.py | 1 + factory/containers.py | 4 ++-- factory/declarations.py | 29 +++++++++++++++-------------- factory/utils.py | 32 ++++++++++++++++++++------------ 4 files changed, 38 insertions(+), 28 deletions(-) (limited to 'factory') diff --git a/factory/__init__.py b/factory/__init__.py index 88865ba..e1138fa 100644 --- a/factory/__init__.py +++ b/factory/__init__.py @@ -50,6 +50,7 @@ from .declarations import ( Dict, List, PostGeneration, + PostGenerationMethodCall, RelatedFactory, ) diff --git a/factory/containers.py b/factory/containers.py index c8be431..ee2ad82 100644 --- a/factory/containers.py +++ b/factory/containers.py @@ -124,7 +124,7 @@ class DeclarationDict(dict): return False elif isinstance(value, declarations.OrderedDeclaration): return True - return (not name.startswith("_")) + return (not name.startswith("_") and not name.startswith("FACTORY_")) def update_with_public(self, d): """Updates the DeclarationDict from a class definition dict. @@ -167,7 +167,7 @@ class PostGenerationDeclarationDict(DeclarationDict): class LazyValue(object): """Some kind of "lazy evaluating" object.""" - def evaluate(self, obj, containers=()): + def evaluate(self, obj, containers=()): # pragma: no cover """Compute the value, using the given object.""" raise NotImplementedError("This is an abstract method.") diff --git a/factory/declarations.py b/factory/declarations.py index 969d780..974b4ac 100644 --- a/factory/declarations.py +++ b/factory/declarations.py @@ -231,13 +231,17 @@ class ContainerAttribute(OrderedDeclaration): return self.function(obj, containers) -class SubFactory(OrderedDeclaration): - """Base class for attributes based upon a sub-factory. +class ParameteredAttribute(OrderedDeclaration): + """Base class for attributes expecting parameters. Attributes: - defaults (dict): Overrides to the defaults defined in the wrapped - factory - factory (base.Factory): the wrapped factory + defaults (dict): Default values for the paramters. + May be overridden by call-time parameters. + + Class attributes: + CONTAINERS_FIELD (str): name of the field, if any, where container + information (e.g for SubFactory) should be stored. If empty, + containers data isn't merged into generate() parameters. """ CONTAINERS_FIELD = '__containers' @@ -248,7 +252,6 @@ class SubFactory(OrderedDeclaration): def __init__(self, **kwargs): super(ParameteredAttribute, self).__init__() self.defaults = kwargs - self.factory = factory def _prepare_containers(self, obj, containers=()): if self.EXTEND_CONTAINERS: @@ -260,19 +263,17 @@ class SubFactory(OrderedDeclaration): """Evaluate the current definition and fill its attributes. Uses attributes definition in the following order: - - attributes defined in the wrapped factory class - - values defined when defining the SubFactory - - additional values defined in attributes + - values defined when defining the ParameteredAttribute + - additional values defined when instantiating the containing factory Args: - create (bool): whether the subfactory should call 'build' or - 'create' + create (bool): whether the parent factory is being 'built' or + 'created' extra (containers.DeclarationDict): extra values that should - override the wrapped factory's defaults + override the defaults containers (list of LazyStub): List of LazyStub for the chain of factories being evaluated, the calling stub being first. """ - defaults = dict(self.defaults) if extra: defaults.update(extra) @@ -393,7 +394,7 @@ class PostGenerationDeclaration(object): kwargs = utils.extract_dict(name, attrs) return extracted, kwargs - def call(self, obj, create, extracted=None, **kwargs): + def call(self, obj, create, extracted=None, **kwargs): # pragma: no cover """Call this hook; no return value is expected. Args: diff --git a/factory/utils.py b/factory/utils.py index f845f46..fb8cfef 100644 --- a/factory/utils.py +++ b/factory/utils.py @@ -58,20 +58,16 @@ def extract_dict(prefix, kwargs, pop=True, exclude=()): return extracted -def declength_compare(a, b): - """Compare objects, choosing longest first.""" - if len(a) > len(b): - return -1 - elif len(a) < len(b): - return 1 - else: - return cmp(a, b) - - def multi_extract_dict(prefixes, kwargs, pop=True, exclude=()): """Extracts all values from a given list of prefixes. - Arguments have the same meaning as for extract_dict. + Extraction will start with longer prefixes. + + Args: + prefixes (str list): the prefixes to use for lookups + kwargs (dict): the dict from which values should be extracted + pop (bool): whether to use pop (True) or get (False) + exclude (iterable): list of prefixed keys that shouldn't be extracted Returns: dict(str => dict): a dict mapping each prefix to the dict of extracted @@ -79,10 +75,22 @@ def multi_extract_dict(prefixes, kwargs, pop=True, exclude=()): """ results = {} exclude = list(exclude) - for prefix in sorted(prefixes, cmp=declength_compare): + for prefix in sorted(prefixes, key=lambda x: -len(x)): extracted = extract_dict(prefix, kwargs, pop=pop, exclude=exclude) results[prefix] = extracted exclude.extend( ['%s%s%s' % (prefix, ATTR_SPLITTER, key) for key in extracted]) return results + + +def import_object(module_name, attribute_name): + """Import an object from its absolute path. + + Example: + >>> import_object('datetime', 'datetime') + + """ + module = __import__(module_name, {}, {}, [attribute_name], 0) + return getattr(module, attribute_name) + -- cgit v1.2.3