[FIX] core; base: lots of docstrings

* add configuration for `flake8[flake8-rst-docstring]`
* enable docstring-related checks
* fix invalid docstrings in odoo's core & `base`
* fix a few more bits (mostly missing or incorrect `:param:` info
  fields) are out of scope for the lint but my editor catches

closes odoo/odoo#74604

Signed-off-by: Xavier Morel (xmo) <xmo@odoo.com>
This commit is contained in:
Xavier Morel
2021-12-09 14:36:58 +00:00
parent bf35fd86d0
commit bdc9d9d369
31 changed files with 529 additions and 405 deletions
+12 -13
View File
@@ -589,22 +589,21 @@ class IrActionsServer(models.Model):
:samp:`_run_action_{TYPE}[_multi]` method is called. This allows easy
overriding of the server actions.
The `_multi` suffix means the runner can operate on multiple records,
The ``_multi`` suffix means the runner can operate on multiple records,
otherwise if there are multiple records the runner will be called once
for each
for each.
:param dict context: context should contain following keys
The call context should contain the following keys:
- active_id: id of the current object (single mode)
- active_model: current model that should equal the action's model
The following keys are optional:
- active_ids: ids of the current records (mass mode). If active_ids
and active_id are present, active_ids is given precedence.
:return: an action_id to be executed, or False is finished correctly without
return action
active_id
id of the current object (single mode)
active_model
current model that should equal the action's model
active_ids (optional)
ids of the current records (mass mode). If ``active_ids`` and
``active_id`` are present, ``active_ids`` is given precedence.
:return: an ``action_id`` to be executed, or ``False`` is finished
correctly without return action
"""
res = False
for action in self.sudo():
+7 -7
View File
@@ -198,7 +198,6 @@ class IrActionsReport(models.Model):
'''Retrieve an attachment for a specific record.
:param record: The record owning of the attachment.
:param attachment_name: The optional name of the attachment.
:return: A recordset of length <=1 or None
'''
attachment_name = safe_eval(self.attachment, {'object': record, 'time': time}) if self.attachment else ''
@@ -211,13 +210,14 @@ class IrActionsReport(models.Model):
], limit=1)
def _postprocess_pdf_report(self, record, buffer):
'''Hook to handle post processing during the pdf report generation.
The basic behavior consists to create a new attachment containing the pdf
base64 encoded.
'''Hook to handle post-processing during the pdf report generation.
:param record_id: The record that will own the attachment.
:param pdf_content: The optional name content of the file to avoid reading both times.
:return: A modified buffer if the previous one has been modified, None otherwise.
The basic behavior consists of creating a new attachment containing the
pdf base64 encoded.
:param record: The record that will own the attachment.
:param io.BytesIO buffer: The PDF content as a bytes buffer.
:return: The input buffer.
'''
attachment_name = safe_eval(self.attachment, {'object': record, 'time': time})
if not attachment_name:
+5
View File
@@ -43,6 +43,9 @@ class IrDefault(models.Model):
scope (field, user, company) will be replaced. The value is encoded
in JSON to be stored to the database.
:param model_name:
:param field_name:
:param value:
:param user_id: may be ``False`` for all users, ``True`` for the
current user, or any user id
:param company_id: may be ``False`` for all companies, ``True`` for
@@ -93,6 +96,8 @@ class IrDefault(models.Model):
""" Return the default value for the given field, user and company, or
``None`` if no default is available.
:param model_name:
:param field_name:
:param user_id: may be ``False`` for all users, ``True`` for the
current user, or any user id
:param company_id: may be ``False`` for all companies, ``True`` for
+2 -2
View File
@@ -84,6 +84,7 @@ class IrFieldsConverter(models.AbstractModel):
records matching what :meth:`odoo.osv.orm.Model.write` expects.
:param model: :class:`odoo.osv.orm.Model` for the conversion base
:param fromtype:
:returns: a converter callable
:rtype: (record: dict, logger: (field, error) -> None) -> dict
"""
@@ -167,11 +168,11 @@ class IrFieldsConverter(models.AbstractModel):
it returns. The handling of a warning at the upper levels is the same
as ``ValueError`` above.
:param model:
:param field: field object to generate a value for
:type field: :class:`odoo.fields.Field`
:param fromtype: type to convert to something fitting for ``field``
:type fromtype: type | str
:param context: odoo request context
:return: a function (fromtype -> field.write_type), if a converter is found
:rtype: Callable | None
"""
@@ -345,7 +346,6 @@ class IrFieldsConverter(models.AbstractModel):
``id`` for an external id and ``.id`` for a database
id
:param value: value of the reference to match to an actual record
:param context: OpenERP request context
:return: a pair of the matched database identifier (if any), the
translated user-readable name for the field and the list of
warnings
+1
View File
@@ -57,6 +57,7 @@ class IrFilters(models.Model):
def get_filters(self, model, action_id=None):
"""Obtain the list of filters available for the user on the given model.
:param int model: id of model to find filters for
:param action_id: optional ID of action to restrict filters to this action
plus global filters. If missing only global filters are returned.
The action does not have to correspond to the model, it may only be
@@ -351,6 +351,8 @@ class IrMailServer(models.Model):
or 'html'). Default is 'plain'.
:param list attachments: list of (filename, filecontents) pairs, where filecontents is a string
containing the bytes of the attachment
:param message_id:
:param references:
:param list email_cc: optional list of string values for CC header (to be joined with commas)
:param list email_bcc: optional list of string values for BCC header (to be joined with commas)
:param dict headers: optional map of headers to set on the outgoing mail (may override the
+4 -3
View File
@@ -717,9 +717,10 @@ class Environment(Mapping):
@contextmanager
def protecting(self, what, records=None):
""" Prevent the invalidation or recomputation of fields on records.
The parameters are either:
- ``what`` a collection of fields and ``records`` a recordset, or
- ``what`` a collection of pairs ``(fields, records)``.
The parameters are either:
- ``what`` a collection of fields and ``records`` a recordset, or
- ``what`` a collection of pairs ``(fields, records)``.
"""
protected = self._protected
try:
+8 -1
View File
@@ -861,6 +861,8 @@ class Field(MetaField('DummyField', (object,), {})):
:meth:`BaseModel.write`. If the value represents a recordset, it should
be added for prefetching on ``record``.
:param value:
:param record:
:param bool validate: when True, field-specific validation of ``value``
will be performed
"""
@@ -886,6 +888,8 @@ class Field(MetaField('DummyField', (object,), {})):
""" Convert ``value`` from the record format to the format returned by
method :meth:`BaseModel.read`.
:param value:
:param record:
:param bool use_name_get: when True, the value's display name will be
computed using :meth:`BaseModel.name_get`, if relevant for the field
"""
@@ -903,6 +907,8 @@ class Field(MetaField('DummyField', (object,), {})):
""" Convert ``value`` from the record format to the format returned by
method :meth:`BaseModel.onchange`.
:param value:
:param record:
:param names: a tree of field names (for relational fields only)
"""
return self.convert_to_read(value, record)
@@ -1048,6 +1054,7 @@ class Field(MetaField('DummyField', (object,), {})):
""" Write the value of ``self`` on ``records``. This method must update
the cache and prepare database updates.
:param records:
:param value: a value in any format
:return: the subset of `records` that have been modified
"""
@@ -3146,7 +3153,7 @@ class Command(enum.IntEnum):
class _RelationalMulti(_Relational):
""" Abstract class for relational fields *2many. """
r"Abstract class for relational fields \*2many."
write_sequence = 20
# Important: the cache contains the ids of all the records in the relation,
+1 -2
View File
@@ -1096,8 +1096,7 @@ class OpenERPSession(sessions.Session):
identifying that action. The method get_action() can be used to get
back the action.
:param the_action: The action to save in the session.
:type the_action: anything
:param action: The action to save in the session.
:return: A key identifying the saved action.
:rtype: integer
"""
+93 -64
View File
@@ -507,7 +507,10 @@ class BaseModel(metaclass=MetaModel):
on the records of the current model using the ``child_of`` and
``parent_of`` domain operators.
"""
_active_name = None #: field to use for active records
_active_name = None
"""field to use for active records, automatically set to either ``"active"``
or ``"x_active"``.
"""
_date_name = 'date' #: field to use for default calendar view
_fold_name = 'fold' #: field to determine folded groups in kanban views
@@ -529,7 +532,9 @@ class BaseModel(metaclass=MetaModel):
# default values for _transient_vacuum()
_transient_max_count = lazy_classproperty(lambda _: config.get('osv_memory_count_limit'))
"maximum number of transient records, unlimited if ``0``"
_transient_max_hours = lazy_classproperty(lambda _: config.get('transient_age_limit'))
"maximum idle lifetime (in hours), unlimited if ``0``"
CONCURRENCY_CHECK_FIELD = '__last_update'
@@ -1041,7 +1046,6 @@ class BaseModel(metaclass=MetaModel):
""" Export fields for selected objects
:param fields_to_export: list of fields
:param raw_data: True to return value in native Python type
:rtype: dictionary with a *datas* matrix
This method is used when exporting data via client menu
@@ -1235,6 +1239,7 @@ class BaseModel(metaclass=MetaModel):
a list of sub-records
The following sub-fields may be set on the record (by key):
* None is the name_get for the record (to use with name_create/name_search)
* "id" is the External ID for the record
* ".id" is the Database ID for the record
@@ -1302,10 +1307,10 @@ class BaseModel(metaclass=MetaModel):
def _convert_records(self, records, log=lambda a: None):
""" Converts records from the source iterable (recursive dicts of
strings) into forms which can be written to the database (via
self.create or (ir.model.data)._update)
``self.create`` or ``(ir.model.data)._update``)
:returns: a list of triplets of (id, xid, record)
:rtype: list((int|None, str|None, dict))
:rtype: list[(int|None, str|None, dict)]
"""
field_names = {name: field.string for name, field in self._fields.items()}
if self.env.lang:
@@ -1614,12 +1619,19 @@ class BaseModel(metaclass=MetaModel):
@api.model
def load_views(self, views, options=None):
""" Returns the fields_views of given views, along with the fields of
the current model, and optionally its filters for the given action.
the current model, and optionally its filters for the given action.
:param views: list of [view_id, view_type]
:param options['toolbar']: True to include contextual actions when loading fields_views
:param options['load_filters']: True to return the model's filters
:param options['action_id']: id of the action to get the filters
:param dict options: a dict optional boolean flags, set to enable:
``toolbar``
includes contextual actions when loading fields_views
``load_filters``
returns the model's filters
``action_id``
id of the action to get the filters, otherwise loads the global
filters or the model
:return: dictionary with fields_views, fields and optionally filters
"""
options = options or {}
@@ -1702,9 +1714,11 @@ class BaseModel(metaclass=MetaModel):
:return: composition of the requested view (including inherited views and extensions)
:rtype: dict
:raise AttributeError:
* if the inherited view has unknown position to work with other than 'before', 'after', 'inside', 'replace'
* if some tag other than 'position' is found in parent view
:raise Invalid ArchitectureError: if there is view type other than form, tree, calendar, search etc defined on the structure
* if the inherited view has unknown position to work with other than 'before', 'after', 'inside', 'replace'
* if some tag other than 'position' is found in parent view
:raise Invalid ArchitectureError: if there is view type other than form, tree, calendar, search etc... defined on the structure
"""
self.check_access_rights('read')
view = self.env['ir.ui.view'].sudo().browse(view_id)
@@ -1739,7 +1753,7 @@ class BaseModel(metaclass=MetaModel):
return result
def get_formview_id(self, access_uid=None):
""" Return an view id to open the document ``self`` with. This method is
""" Return a view id to open the document ``self`` with. This method is
meant to be overridden in addons that want to give specific view ids
for example.
@@ -1770,7 +1784,7 @@ class BaseModel(metaclass=MetaModel):
def get_access_action(self, access_uid=None):
""" Return an action to open the document. This method is meant to be
overridden in addons that want to give specific access to the document.
By default it opens the formview of the document.
By default, it opens the formview of the document.
An optional access_uid holds the user that will access the document
that could be different from the current user.
@@ -1804,7 +1818,6 @@ class BaseModel(metaclass=MetaModel):
:param str order: sort string
:param bool count: if True, only counts and returns the number of matching records (default: False)
:returns: at most ``limit`` records matching the search criteria
:raise AccessError: * if user tries to bypass access rules for read on the requested object.
"""
res = self._search(args, offset=offset, limit=limit, order=order, count=count)
@@ -1842,7 +1855,7 @@ class BaseModel(metaclass=MetaModel):
:attr:`~.display_name` it is important that it resets to the
"default" behaviour if the context keys are empty / missing.
:return: list of pairs ``(id, text_repr)`` for each records
:return: list of pairs ``(id, text_repr)`` for each record
:rtype: list[(int, str)]
"""
result = []
@@ -1888,8 +1901,8 @@ class BaseModel(metaclass=MetaModel):
matching the optional search domain (``args``).
This is used for example to provide suggestions based on a partial
value for a relational field. Sometimes be seen as the inverse
function of :meth:`~.name_get`, but it is not guaranteed to be.
value for a relational field. Should usually behave as the reverse of
:meth:`~.name_get`, but that is ont guaranteed.
This method is equivalent to calling :meth:`~.search` with a search
domain based on ``display_name`` and then :meth:`~.name_get` on the
@@ -2068,7 +2081,8 @@ class BaseModel(metaclass=MetaModel):
Assume we group records by month, and we only have data for June,
September and December. By default, plotting the result gives something
like:
like::
___
___ | |
| | ___ | |
@@ -2077,7 +2091,8 @@ class BaseModel(metaclass=MetaModel):
The problem is that December data immediately follow September data,
which is misleading for the user. Adding explicit zeroes for missing
data gives something like:
data gives something like::
___
___ | |
| | ___ | |
@@ -2098,7 +2113,8 @@ class BaseModel(metaclass=MetaModel):
group with data.
If we want to fill groups only between August (fill_from)
and October (fill_to):
and October (fill_to)::
___
___ | |
| | ___ | |
@@ -2106,9 +2122,9 @@ class BaseModel(metaclass=MetaModel):
Jun Aug Sep Oct Dec
We still get June and December. To filter them out, we should match
`fill_from` and `fill_to` with the domain e.g. ['&',
('date_field', '>=', 'YYYY-08-01'),
('date_field', '<', 'YYYY-11-01')]:
`fill_from` and `fill_to` with the domain e.g. ``['&',
('date_field', '>=', 'YYYY-08-01'), ('date_field', '<', 'YYYY-11-01')]``::
___
___ |___| ___
Aug Sep Oct
@@ -2123,7 +2139,8 @@ class BaseModel(metaclass=MetaModel):
existing group. If neither `fill_from` nor `fill_to` is specified, and
there is no existing group, no group will be returned.
If we set min_groups = 4:
If we set min_groups = 4::
___
___ |___| ___ ___
Aug Sep Oct Nov
@@ -2210,12 +2227,15 @@ class BaseModel(metaclass=MetaModel):
"""
Prepares the GROUP BY and ORDER BY terms for the read_group method. Adds the missing JOIN clause
to the query if order should be computed against m2o field.
:param orderby: the orderby definition in the form "%(field)s %(order)s"
:param aggregated_fields: list of aggregated fields in the query
:param annotated_groupbys: list of dictionaries returned by _read_group_process_groupby
These dictionaries contains the qualified name of each groupby
(fully qualified SQL name for the corresponding field),
and the (non raw) field name.
:param annotated_groupbys: list of dictionaries returned by
:meth:`_read_group_process_groupby`
These dictionaries contain the qualified name of each groupby
(fully qualified SQL name for the corresponding field),
and the (non raw) field name.
:param osv.Query query: the query under construction
:return: (groupby_terms, orderby_terms)
"""
@@ -2436,7 +2456,7 @@ class BaseModel(metaclass=MetaModel):
* __range: (date/datetime only) dictionary with field names as keys mapping to
a dictionary with keys: "from" (inclusive) and "to" (exclusive)
mapping to a string representation of the temporal bounds of the group
:rtype: [{'field_name_1': value, ...]
:rtype: [{'field_name_1': value, ...}, ...]
:raise AccessError: * if user has no read rights on the requested object
* if user tries to bypass access rules for read on the requested object
"""
@@ -2878,10 +2898,8 @@ class BaseModel(metaclass=MetaModel):
_logger.warning("parent_path field on model %r should have unaccent disabled. Add `unaccent=False` to the field definition.", self._name)
def _add_sql_constraints(self):
"""
Modify this model's database table constraints so they match the one in
_sql_constraints.
""" Modify this model's database table constraints so they match the one
in _sql_constraints.
"""
cr = self._cr
@@ -2908,7 +2926,7 @@ class BaseModel(metaclass=MetaModel):
self._cr.execute(self._sql)
#
# Update objects that uses this one to update their _inherits fields
# Update objects that use this one to update their _inherits fields
#
@api.model
@@ -3217,6 +3235,8 @@ Fields:
method. In Python code, prefer :meth:`~.browse`.
:param fields: list of field names to return (default is all fields)
:param load: loading mode, currently the only option is to set to
``None`` to avoid loading the ``name_get`` of m2o fields
:return: a list of dictionaries mapping field names to their values,
with one dictionary per record
:raise AccessError: if user has no read rights on some of the given
@@ -3436,7 +3456,7 @@ Fields:
"""
Returns rooturl for a specific given record.
By default, it return the ir.config.parameter of base_url
By default, it returns the ir.config.parameter of base_url
but it can be overridden by model.
:return: the base url for this record
@@ -3558,7 +3578,7 @@ Fields:
the current user according to ir.rules.
:param operation: one of ``write``, ``unlink``
:raise UserError: * if current ir.rules do not permit this operation.
:raise UserError: * if current ``ir.rules`` do not permit this operation.
:return: None if the operation is allowed
"""
if self.env.su:
@@ -4503,10 +4523,10 @@ Fields:
@api.model
def _where_calc(self, domain, active_test=True):
"""Computes the WHERE clause needed to implement an OpenERP domain.
:param domain: the domain to compute
:type domain: list
:param active_test: whether the default filtering of records with ``active``
field set to ``False`` should be applied.
:param list domain: the domain to compute
:param bool active_test: whether the default filtering of records with
``active`` field set to ``False`` should be applied.
:return: the query expressing the given domain as provided in domain
:rtype: osv.query.Query
"""
@@ -5088,8 +5108,9 @@ Fields:
Defaults to no limit.
:param order: Columns to sort result, see ``order`` parameter in :meth:`search`.
Defaults to no sort.
:param read_kwargs: All read keywords arguments used to call read(..., **read_kwargs) method
E.g. you can use search_read(..., load='') in order to avoid computing name_get
:param read_kwargs: All read keywords arguments used to call
``read(..., **read_kwargs)`` method e.g. you can use
``search_read(..., load='')`` in order to avoid computing name_get
:return: List of dictionaries containing the asked fields.
:rtype: list(dict).
"""
@@ -5119,20 +5140,20 @@ Fields:
return [index[record.id] for record in records if record.id in index]
def toggle_active(self):
""" Inverse the value of the field ``(x_)active`` on the records in ``self``. """
"Inverses the value of :attr:`active` on the records in ``self``."
active_recs = self.filtered(self._active_name)
active_recs[self._active_name] = False
(self - active_recs)[self._active_name] = True
def action_archive(self):
""" Set (x_)active=False on a recordset, by calling toggle_active to
take the corresponding actions according to the model
"""Sets :attr:`active` to ``False`` on a recordset, by calling
:meth:`toggle_active` on its currently active records.
"""
return self.filtered(lambda record: record[self._active_name]).toggle_active()
def action_unarchive(self):
""" Set (x_)active=True on a recordset, by calling toggle_active to
take the corresponding actions according to the model
"""Sets :attr:`active` to ``True`` on a recordset, by calling
:meth:`toggle_active` on its currently inactive records.
"""
return self.filtered(lambda record: not record[self._active_name]).toggle_active()
@@ -5356,7 +5377,7 @@ Fields:
return self.with_context(allowed_company_ids=allowed_company_ids)
def with_context(self, *args, **kwargs):
""" with_context([context][, **overrides]) -> records
""" with_context([context][, **overrides]) -> Model
Returns a new version of this recordset attached to an extended
context.
@@ -5374,7 +5395,7 @@ Fields:
.. note:
The returned recordset has the same prefetch object as ``self``.
"""
""" # noqa: RST210
if (args and 'force_company' in args[0]) or 'force_company' in kwargs:
_logger.warning(
"Context key 'force_company' is no longer supported. "
@@ -5684,9 +5705,9 @@ Fields:
""" Process all the pending computations (on all models), and flush all
the pending updates to the database.
:param fnames (list<str>): list of field names to flush. If given,
:param list[str] fnames: list of field names to flush. If given,
limit the processing to the given fields of the current model.
:param records (Model): if given (together with ``fnames``), limit the
:param Model records: if given (together with ``fnames``), limit the
processing to the given records.
"""
def process(model, id_vals):
@@ -6080,7 +6101,7 @@ Fields:
recursively_marked = self.env.not_to_compute(field, records)
self.env.add_to_compute(field, records)
else:
# Dont force the recomputation of compute fields which are
# Don't force the recomputation of compute fields which are
# not stored as this is not really necessary.
if field.recursive:
recursively_marked = records & self.env.cache.get_records(records, field)
@@ -6423,7 +6444,7 @@ Fields:
names.append(name)
# prefetch x2many lines: this speeds up the initial snapshot by avoiding
# to compute fields on new records as much as possible, as that can be
# computing fields on new records as much as possible, as that can be
# costly and is not necessary at all
for name, subnames in nametree.items():
if subnames and values.get(name):
@@ -6742,18 +6763,26 @@ class TransientModel(Model):
"""Clean the transient records.
This unlinks old records from the transient model tables whenever the
"_transient_max_count" or "_max_age" conditions (if any) are reached.
Actual cleaning will happen only once every "_transient_check_time" calls.
This means this method can be called frequently called (e.g. whenever
a new record is created).
:attr:`_transient_max_count` or :attr:`_transient_max_hours` conditions
(if any) are reached.
Actual cleaning will happen only once every 5 minutes. This means this
method can be called frequently (e.g. whenever a new record is created).
Example with both max_hours and max_count active:
Suppose max_hours = 0.2 (e.g. 12 minutes), max_count = 20, there are 55 rows in the
table, 10 created/changed in the last 5 minutes, an additional 12 created/changed between
5 and 10 minutes ago, the rest created/changed more then 12 minutes ago.
- age based vacuum will leave the 22 rows created/changed in the last 12 minutes
- count based vacuum will wipe out another 12 rows. Not just 2, otherwise each addition
would immediately cause the maximum to be reached again.
- the 10 rows that have been created/changed the last 5 minutes will NOT be deleted
Suppose max_hours = 0.2 (aka 12 minutes), max_count = 20, there are
55 rows in the table, 10 created/changed in the last 5 minutes, an
additional 12 created/changed between 5 and 10 minutes ago, the rest
created/changed more than 12 minutes ago.
- age based vacuum will leave the 22 rows created/changed in the last 12
minutes
- count based vacuum will wipe out another 12 rows. Not just 2,
otherwise each addition would immediately cause the maximum to be
reached again.
- the 10 rows that have been created/changed the last 5 minutes will NOT
be deleted
"""
if self._transient_max_hours:
# Age-based expiration
+4
View File
@@ -123,11 +123,15 @@ def force_demo(cr):
def load_module_graph(cr, graph, status=None, perform_checks=True,
skip_modules=None, report=None, models_to_check=None):
"""Migrates+Updates or Installs all module nodes from ``graph``
:param cr:
:param graph: graph of module nodes to load
:param status: deprecated parameter, unused, left to avoid changing signature in 8.0
:param perform_checks: whether module descriptors should be checked for validity (prints warnings
for same cases)
:param skip_modules: optional list of module names (packages) which have previously been loaded and can be skipped
:param report:
:param set models_to_check:
:return: list of modules that were installed or updated
"""
if models_to_check is None:
+11 -8
View File
@@ -27,18 +27,21 @@ def load_script(path, module_name):
class MigrationManager(object):
"""
This class manage the migration of modules
Migrations files must be python files containing a `migrate(cr, installed_version)`
""" Manages the migration of modules.
Migrations files must be python files containing a ``migrate(cr, installed_version)``
function. These files must respect a directory tree structure: A 'migrations' folder
which contains a folder by version. Version can be 'module' version or 'server.module'
version (in this case, the files will only be processed by this version of the server).
Python file names must start by `pre-` or `post-` and will be executed, respectively,
before and after the module initialisation. `end-` scripts are run after all modules have
Python file names must start by ``pre-`` or ``post-`` and will be executed, respectively,
before and after the module initialisation. ``end-`` scripts are run after all modules have
been updated.
A special folder named `0.0.0` can contain scripts that will be run on any version change.
In `pre` stage, `0.0.0` scripts are run first, while in `post` and `end`, they are run last.
Example:
A special folder named ``0.0.0`` can contain scripts that will be run on any version change.
In `pre` stage, ``0.0.0`` scripts are run first, while in ``post`` and ``end``, they are run last.
Example::
<moduledir>
`-- migrations
|-- 1.0
+48 -36
View File
@@ -244,6 +244,7 @@ def combine(operator, unit, zero, domains):
It is guaranteed to return a normalized domain.
:param operator:
:param unit: the identity element of the domains "set" with regard to the operation
performed by ``operator``, i.e the domain component ``i`` which, when
combined with any domain ``x`` via ``operator``, yields ``x``.
@@ -371,12 +372,13 @@ def is_operator(element):
def is_leaf(element, internal=False):
""" Test whether an object is a valid domain term:
- is a list or tuple
- with 3 elements
- second element if a valid op
:param tuple element: a leaf in form (left, operator, right)
:param boolean internal: allow or not the 'inselect' internal operator
:param bool internal: allow or not the 'inselect' internal operator
in the term. This should be always left to False.
Note: OLD TODO change the share wizard to use this function.
@@ -470,48 +472,58 @@ class expression(object):
def parse(self):
""" Transform the leaves of the expression
The principle is to pop elements from a leaf stack one at a time.
Each leaf is processed. The processing is a if/elif list of various
cases that appear in the leafs (many2one, function fields, ...).
Three things can happen as a processing result:
- the leaf is a logic operator, and updates the result stack
accordingly;
- the leaf has been modified and/or new leafs have to be introduced
in the expression; they are pushed into the leaf stack, to be
processed right after;
- the leaf is converted to SQL and added to the result stack
The principle is to pop elements from a leaf stack one at a time.
Each leaf is processed. The processing is a if/elif list of various
cases that appear in the leafs (many2one, function fields, ...).
Here is a suggested execution:
Three things can happen as a processing result:
step stack result_stack
- the leaf is a logic operator, and updates the result stack
accordingly;
- the leaf has been modified and/or new leafs have to be introduced
in the expression; they are pushed into the leaf stack, to be
processed right after;
- the leaf is converted to SQL and added to the result stack
['&', A, B] []
substitute B ['&', A, B1] []
convert B1 in SQL ['&', A] ["B1"]
substitute A ['&', '|', A1, A2] ["B1"]
convert A2 in SQL ['&', '|', A1] ["B1", "A2"]
convert A1 in SQL ['&', '|'] ["B1", "A2", "A1"]
apply operator OR ['&'] ["B1", "A1 or A2"]
apply operator AND [] ["(A1 or A2) and B1"]
Example:
Some internal var explanation:
:var list path: left operand seen as a sequence of field names
("foo.bar" -> ["foo", "bar"])
:var obj model: model object, model containing the field
(the name provided in the left operand)
:var obj field: the field corresponding to `path[0]`
:var obj column: the column corresponding to `path[0]`
:var obj comodel: relational model of field (field.comodel)
(res_partner.bank_ids -> res.partner.bank)
=================== =================== =====================
step stack result_stack
=================== =================== =====================
['&', A, B] []
substitute B ['&', A, B1] []
convert B1 in SQL ['&', A] ["B1"]
substitute A ['&', '|', A1, A2] ["B1"]
convert A2 in SQL ['&', '|', A1] ["B1", "A2"]
convert A1 in SQL ['&', '|'] ["B1", "A2", "A1"]
apply operator OR ['&'] ["B1", "A1 or A2"]
apply operator AND [] ["(A1 or A2) and B1"]
=================== =================== =====================
Some internal var explanation:
:var list path: left operand seen as a sequence of field names
("foo.bar" -> ["foo", "bar"])
:var obj model: model object, model containing the field
(the name provided in the left operand)
:var obj field: the field corresponding to `path[0]`
:var obj column: the column corresponding to `path[0]`
:var obj comodel: relational model of field (field.comodel)
(res_partner.bank_ids -> res.partner.bank)
"""
def to_ids(value, comodel, leaf):
""" Normalize a single id or name, or a list of those, into a list of ids
:param {int,long,basestring,list,tuple} value:
if int, long -> return [value]
if basestring, convert it into a list of basestrings, then
if list of basestring ->
perform a name_search on comodel for each name
return the list of related ids
:param comodel:
:param leaf:
:param int|str|list|tuple value:
- if int, long -> return [value]
- if basestring, convert it into a list of basestrings, then
- if list of basestring ->
- perform a name_search on comodel for each name
- return the list of related ids
"""
names = []
if isinstance(value, str):
+5 -3
View File
@@ -22,14 +22,16 @@ def _from_table(table, alias):
def _generate_table_alias(src_table_alias, link):
""" Generate a standard table alias name. An alias is generated as following:
- the base is the source table name (that can already be an alias)
- then, the joined table is added in the alias using a 'link field name'
that is used to render unique aliases for a given path
- the name is shortcut if it goes beyond PostgreSQL's identifier limits
Examples:
- src_table_alias='res_users', link='parent_id'
alias = 'res_users__parent_id'
.. code-block:: pycon
>>> _generate_table_alias('res_users', link='parent_id')
'res_users__parent_id'
:param str src_table_alias: alias of the source table
:param str link: field name
+15 -13
View File
@@ -569,19 +569,21 @@ class TestCursor(BaseCursor):
the transaction open across requests, and simulates committing, rolling
back, and closing:
test cursor | queries on actual cursor
------------------------+---------------------------------------
cr = TestCursor(...) | SAVEPOINT test_cursor_N
|
cr.execute(query) | query
|
cr.commit() | RELEASE SAVEPOINT test_cursor_N
| SAVEPOINT test_cursor_N (lazy)
|
cr.rollback() | ROLLBACK TO SAVEPOINT test_cursor_N (if savepoint)
|
cr.close() | ROLLBACK TO SAVEPOINT test_cursor_N (if savepoint)
| RELEASE SAVEPOINT test_cursor_N (if savepoint)
+------------------------+---------------------------------------------------+
| test cursor | queries on actual cursor |
+========================+===================================================+
|``cr = TestCursor(...)``| SAVEPOINT test_cursor_N |
+------------------------+---------------------------------------------------+
| ``cr.execute(query)`` | query |
+------------------------+---------------------------------------------------+
| ``cr.commit()`` | RELEASE SAVEPOINT test_cursor_N |
| | SAVEPOINT test_cursor_N (lazy) |
+------------------------+---------------------------------------------------+
| ``cr.rollback()`` | ROLLBACK TO SAVEPOINT test_cursor_N (if savepoint)|
+------------------------+---------------------------------------------------+
| ``cr.close()`` | ROLLBACK TO SAVEPOINT test_cursor_N (if savepoint)|
| | RELEASE SAVEPOINT test_cursor_N (if savepoint) |
+------------------------+---------------------------------------------------+
"""
_cursors_stack = []
def __init__(self, cursor, lock):
+21 -13
View File
@@ -305,11 +305,10 @@ class MetaCase(type):
def _normalize_arch_for_assert(arch_string, parser_method="xml"):
"""Takes some xml and normalize it to make it comparable to other xml
in particular, blank text is removed, and the output is pretty-printed
:param arch_string: the string representing an XML arch
:type arch_string: str
:param parser_method: an string representing which lxml.Parser class to use
:param str arch_string: the string representing an XML arch
:param str parser_method: an string representing which lxml.Parser class to use
when normalizing both archs. Takes either "xml" or "html"
:type parser_method: str
:return: the normalized arch
:rtype str:
"""
@@ -671,6 +670,7 @@ class BaseCase(unittest.TestCase, metaclass=MetaCase):
def _assertXMLEqual(self, original, expected, parser="xml"):
"""Asserts that two xmls archs are equal
:param original: the xml arch to test
:type original: str
:param expected: the xml arch of reference
@@ -981,16 +981,24 @@ class ChromeBrowser:
self._logger.info('Chrome headless temporary user profile dir: %s', self.user_data_dir)
def _json_command(self, command, timeout=3, get_key=None):
"""
Inspect dev tools with get
"""Queries browser state using JSON
Available commands:
'' : return list of tabs with their id
list (or json/): list tabs
new : open a new tab
activate/ + an id: activate a tab
close/ + and id: close a tab
version : get chrome and dev tools version
protocol : get the full protocol
``''``
return list of tabs with their id
``list`` (or ``json/``)
list tabs
``new``
open a new tab
:samp:`activate/{id}`
activate a tab
:samp:`close/{id}`
close a tab
``version``
get chrome and dev tools version
``protocol``
get the full protocol
"""
command = os.path.join('json', command).strip('/')
url = werkzeug.urls.url_join('http://%s:%s/' % (HOST, self.devtools_port), command)
+96 -74
View File
@@ -86,20 +86,26 @@ def site_data_dir(appname=None, appauthor=None, version=None, multipath=False):
of your app to be able to run independently. If used, this
would typically be "<major>.<minor>".
Only applied when appname is present.
"multipath" is an optional parameter only applicable to *nix
"multipath" is an optional parameter only applicable to \*nix
which indicates that the entire list of data dirs should be
returned. By default, the first item from XDG_DATA_DIRS is
returned, or '/usr/local/share/<AppName>',
if XDG_DATA_DIRS is not set
returned, or :samp:`/usr/local/share/{AppName}`,
if ``XDG_DATA_DIRS`` is not set
Typical user data directories are:
Mac OS X: /Library/Application Support/<AppName>
Unix: /usr/local/share/<AppName> or /usr/share/<AppName>
Win XP: C:\Documents and Settings\All Users\Application Data\<AppAuthor>\<AppName>
Vista: (Fail! "C:\ProgramData" is a hidden *system* directory on Vista.)
Win 7: C:\ProgramData\<AppAuthor>\<AppName> # Hidden, but writeable on Win 7.
For Unix, this is using the $XDG_DATA_DIRS[0] default.
Mac OS X
:samp:`/Library/Application Support/{AppName}`
Unix
:samp:`/usr/local/share/{AppName}` or :samp:`/usr/share/{AppName}`
Win XP
:samp:`C:\Documents and Settings\All Users\Application Data\{AppAuthor}\{AppName}`
Vista
Fail! "C:\ProgramData" is a hidden *system* directory on Vista.
Win 7
:samp:`C:\ProgramData\{AppAuthor}\{AppName}` (hidden, but writeable on Win 7)
For Unix, this is using the ``$XDG_DATA_DIRS[0]`` default.
WARNING: Do not use this on Windows. See the Vista-Fail note above for why.
"""
@@ -136,32 +142,36 @@ def site_data_dir(appname=None, appauthor=None, version=None, multipath=False):
def user_config_dir(appname=None, appauthor=None, version=None, roaming=False):
r"""Return full path to the user-specific config dir for this application.
"""Return full path to the user-specific config dir for this application.
"appname" is the name of application.
If None, just the system directory is returned.
"appauthor" (only required and used on Windows) is the name of the
appauthor or distributing body for this application. Typically
it is the owning company name. This falls back to appname.
"version" is an optional version path element to append to the
path. You might want to use this if you want multiple versions
of your app to be able to run independently. If used, this
would typically be "<major>.<minor>".
Only applied when appname is present.
"roaming" (boolean, default False) can be set True to use the Windows
roaming appdata directory. That means that for users on a Windows
network setup for roaming profiles, this user data will be
sync'd on login. See
<http://technet.microsoft.com/en-us/library/cc766489(WS.10).aspx>
for a discussion of issues.
"appname" is the name of application.
If None, just the system directory is returned.
"appauthor" (only required and used on Windows) is the name of the
appauthor or distributing body for this application. Typically
it is the owning company name. This falls back to appname.
"version" is an optional version path element to append to the
path. You might want to use this if you want multiple versions
of your app to be able to run independently. If used, this
would typically be "<major>.<minor>".
Only applied when appname is present.
"roaming" (boolean, default False) can be set True to use the Windows
roaming appdata directory. That means that for users on a Windows
network setup for roaming profiles, this user data will be
sync'd on login. See `managing roaming user data
<http://technet.microsoft.com/en-us/library/cc766489(WS.10).aspx>`_
for a discussion of issues.
Typical user data directories are:
Mac OS X: same as user_data_dir
Unix: ~/.config/<AppName> # or in $XDG_CONFIG_HOME, if defined
Win *: same as user_data_dir
For Unix, we follow the XDG spec and support $XDG_DATA_HOME.
That means, by default "~/.local/share/<AppName>".
Mac OS X
same as user_data_dir
Unix
:samp:`~/.config/{AppName}` or in $XDG_CONFIG_HOME, if defined
Win *
same as user_data_dir
For Unix, we follow the XDG spec and support ``$XDG_DATA_HOME``.
That means, by default :samp:`~/.local/share/{AppName}`.
"""
if sys.platform in [ "win32", "darwin" ]:
path = user_data_dir(appname, appauthor, None, roaming)
@@ -177,29 +187,34 @@ def user_config_dir(appname=None, appauthor=None, version=None, roaming=False):
def site_config_dir(appname=None, appauthor=None, version=None, multipath=False):
"""Return full path to the user-shared data dir for this application.
"appname" is the name of application.
If None, just the system directory is returned.
"appauthor" (only required and used on Windows) is the name of the
appauthor or distributing body for this application. Typically
it is the owning company name. This falls back to appname.
"version" is an optional version path element to append to the
path. You might want to use this if you want multiple versions
of your app to be able to run independently. If used, this
would typically be "<major>.<minor>".
Only applied when appname is present.
"multipath" is an optional parameter only applicable to *nix
which indicates that the entire list of config dirs should be
returned. By default, the first item from XDG_CONFIG_DIRS is
returned, or '/etc/xdg/<AppName>', if XDG_CONFIG_DIRS is not set
"appname" is the name of application.
If None, just the system directory is returned.
"appauthor" (only required and used on Windows) is the name of the
appauthor or distributing body for this application. Typically
it is the owning company name. This falls back to appname.
"version" is an optional version path element to append to the
path. You might want to use this if you want multiple versions
of your app to be able to run independently. If used, this
would typically be "<major>.<minor>".
Only applied when appname is present.
"multipath" is an optional parameter only applicable to \*nix
which indicates that the entire list of config dirs should be
returned. By default, the first item from ``XDG_CONFIG_DIRS`` is
returned, or :samp:`/etc/xdg/{AppName}`, if ``XDG_CONFIG_DIRS`` is not set
Typical user data directories are:
Mac OS X: same as site_data_dir
Unix: /etc/xdg/<AppName> or $XDG_CONFIG_DIRS[i]/<AppName> for each value in
$XDG_CONFIG_DIRS
Win *: same as site_data_dir
Vista: (Fail! "C:\ProgramData" is a hidden *system* directory on Vista.)
For Unix, this is using the $XDG_CONFIG_DIRS[0] default, if multipath=False
Mac OS X
same as site_data_dir
Unix
``/etc/xdg/<AppName>`` or ``$XDG_CONFIG_DIRS[i]/<AppName>`` for each
value in ``$XDG_CONFIG_DIRS``
Win *
same as site_data_dir
Vista
Fail! "C:\ProgramData" is a hidden *system* directory on Vista.
For Unix, this is using the ``$XDG_CONFIG_DIRS[0]`` default, if ``multipath=False``
WARNING: Do not use this on Windows. See the Vista-Fail note above for why.
"""
@@ -226,34 +241,41 @@ def site_config_dir(appname=None, appauthor=None, version=None, multipath=False)
def user_cache_dir(appname=None, appauthor=None, version=None, opinion=True):
r"""Return full path to the user-specific cache dir for this application.
"appname" is the name of application.
If None, just the system directory is returned.
"appauthor" (only required and used on Windows) is the name of the
appauthor or distributing body for this application. Typically
it is the owning company name. This falls back to appname.
"version" is an optional version path element to append to the
path. You might want to use this if you want multiple versions
of your app to be able to run independently. If used, this
would typically be "<major>.<minor>".
Only applied when appname is present.
"opinion" (boolean) can be False to disable the appending of
"Cache" to the base app data dir for Windows. See
discussion below.
"appname" is the name of application.
If None, just the system directory is returned.
"appauthor" (only required and used on Windows) is the name of the
appauthor or distributing body for this application. Typically
it is the owning company name. This falls back to appname.
"version" is an optional version path element to append to the
path. You might want to use this if you want multiple versions
of your app to be able to run independently. If used, this
would typically be "<major>.<minor>".
Only applied when appname is present.
"opinion" (boolean) can be False to disable the appending of
"Cache" to the base app data dir for Windows. See
discussion below.
Typical user cache directories are:
Mac OS X: ~/Library/Caches/<AppName>
Unix: ~/.cache/<AppName> (XDG default)
Win XP: C:\Documents and Settings\<username>\Local Settings\Application Data\<AppAuthor>\<AppName>\Cache
Vista: C:\Users\<username>\AppData\Local\<AppAuthor>\<AppName>\Cache
Mac OS X
~/Library/Caches/<AppName>
Unix
~/.cache/<AppName> (XDG default)
Win XP
C:\Documents and Settings\<username>\Local Settings\Application Data\<AppAuthor>\<AppName>\Cache
Vista
C:\Users\<username>\AppData\Local\<AppAuthor>\<AppName>\Cache
On Windows the only suggestion in the MSDN docs is that local settings go in
the `CSIDL_LOCAL_APPDATA` directory. This is identical to the non-roaming
app data dir (the default returned by `user_data_dir` above). Apps typically
the ``CSIDL_LOCAL_APPDATA`` directory. This is identical to the non-roaming
app data dir (the default returned by ``user_data_dir`` above). Apps typically
put cache data somewhere *under* the given dir here. Some examples:
...\Mozilla\Firefox\Profiles\<ProfileName>\Cache
...\Acme\SuperApp\Cache\1.0
OPINION: This function appends "Cache" to the `CSIDL_LOCAL_APPDATA` value.
This can be disabled with the `opinion=False` option.
- ...\Mozilla\Firefox\Profiles\<ProfileName>\Cache
- ...\Acme\SuperApp\Cache\1.0
OPINION: This function appends "Cache" to the ``CSIDL_LOCAL_APPDATA`` value.
This can be disabled with the ``opinion=False`` option.
"""
if sys.platform == "win32":
if appauthor is None:
+1 -1
View File
@@ -291,7 +291,7 @@ class configmanager(object):
"[ipython|ptpython|bpython|python]")
group.add_option("--stop-after-init", action="store_true", dest="stop_after_init", my_default=False,
help="stop the server after its initialization")
group.add_option("--osv-memory-count-limit", dest="osv_memory_count_limit", my_default=False,
group.add_option("--osv-memory-count-limit", dest="osv_memory_count_limit", my_default=0,
help="Force a limit on the maximum number of records kept in the virtual "
"osv_memory tables. By default there is no limit.",
type="int")
+3 -3
View File
@@ -216,9 +216,9 @@ def json_default(obj):
def date_range(start, end, step=relativedelta(months=1)):
"""Date range generator with a step interval.
:param start datetime: beginning date of the range.
:param end datetime: ending date of the range.
:param step relativedelta: interval of the range.
:param datetime start: beginning date of the range.
:param datetime end: ending date of the range.
:param relativedelta step: interval of the range.
:return: a range of datetime from start to end.
:rtype: Iterator[datetime]
"""
+4 -4
View File
@@ -162,13 +162,13 @@ def float_compare(value1, value2, precision_digits=None, precision_rounding=None
def float_repr(value, precision_digits):
"""Returns a string representation of a float with the
the given number of fractional digits. This should not be
given number of fractional digits. This should not be
used to perform a rounding operation (this is done via
:meth:`~.float_round`), but only to produce a suitable
:func:`~.float_round`), but only to produce a suitable
string representation for a float.
:param int precision_digits: number of fractional digits to
include in the output
:param float value:
:param int precision_digits: number of fractional digits to include in the output
"""
# Can't use str() here because it seems to have an intrinsic
# rounding to 12 significant digits, which causes a loss of
+6 -5
View File
@@ -50,7 +50,7 @@ class lazy_classproperty(lazy_property):
def conditional(condition, decorator):
""" Decorator for a conditionally applied decorator.
Example:
Example::
@conditional(get_config('use_cache'), ormcache)
def fn():
@@ -118,12 +118,13 @@ def classproperty(func):
class lazy(object):
""" A proxy to the (memoized) result of a lazy evaluation::
""" A proxy to the (memoized) result of a lazy evaluation:
foo = lazy(func, arg) # func(arg) is not called yet
bar = foo + 1 # eval func(arg) and add 1
baz = foo + 2 # use result of func(arg) and add 2
.. code-block::
foo = lazy(func, arg) # func(arg) is not called yet
bar = foo + 1 # eval func(arg) and add 1
baz = foo + 2 # use result of func(arg) and add 2
"""
__slots__ = ['_func', '_args', '_kwargs', '_cached_value']
+48 -74
View File
@@ -53,6 +53,7 @@ class ImageProcess():
"""Initialize the `base64_source` image for processing.
:param base64_source: the original image base64 encoded
No processing will be done if the `base64_source` is falsy or if
the image is SVG.
:type base64_source: string or bytes
@@ -61,8 +62,6 @@ class ImageProcess():
excessive before starting to process it. The max allowed resolution is
defined by `IMAGE_MAX_RESOLUTION`.
:type verify_resolution: bool
:return: self
:rtype: ImageProcess
:raise: ValueError if `verify_resolution` is True and the image is too large
@@ -99,19 +98,16 @@ class ImageProcess():
and the `output_format` is the same as the original format and the
quality is not specified.
:param quality: quality setting to apply. Default to 0.
:param int quality: quality setting to apply. Default to 0.
- for JPEG: 1 is worse, 95 is best. Values above 95 should be
avoided. Falsy values will fallback to 95, but only if the image
was changed, otherwise the original image is returned.
avoided. Falsy values will fallback to 95, but only if the image
was changed, otherwise the original image is returned.
- for PNG: set falsy to prevent conversion to a WEB palette.
- for other formats: no effect.
:type quality: int
:param output_format: the output format. Can be PNG, JPEG, GIF, or ICO.
:param str output_format: the output format. Can be PNG, JPEG, GIF, or ICO.
Default to the format of the original image. BMP is converted to
PNG, other formats than those mentioned above are converted to JPEG.
:type output_format: string
:return: image
:rtype: bytes or False
"""
@@ -160,19 +156,16 @@ class ImageProcess():
applied and the `output_format` is the same as the original format and
the quality is not specified.
:param quality: quality setting to apply. Default to 0.
:param int quality: quality setting to apply. Default to 0.
- for JPEG: 1 is worse, 95 is best. Values above 95 should be
avoided. Falsy values will fallback to 95, but only if the image
was changed, otherwise the original image is returned.
avoided. Falsy values will fallback to 95, but only if the image
was changed, otherwise the original image is returned.
- for PNG: set falsy to prevent conversion to a WEB palette.
- for other formats: no effect.
:type quality: int
:param output_format: the output format. Can be PNG, JPEG, GIF, or ICO.
:param str output_format: the output format. Can be PNG, JPEG, GIF, or ICO.
Default to the format of the original image. BMP is converted to
PNG, other formats than those mentioned above are converted to JPEG.
:type output_format: string
:return: image base64 encoded or False
:rtype: bytes or False
"""
@@ -200,12 +193,8 @@ class ImageProcess():
It is currently not supported for GIF because we do not handle all the
frames properly.
:param max_width: max width
:type max_width: int
:param max_height: max height
:type max_height: int
:param int max_width: max width
:param int max_height: max height
:return: self to allow chaining
:rtype: ImageProcess
"""
@@ -238,20 +227,12 @@ class ImageProcess():
It is currently not supported for GIF because we do not handle all the
frames properly.
:param max_width: max width
:type max_width: int
:param max_height: max height
:type max_height: int
:param center_x: the center of the crop between 0 (left) and 1 (right)
Default to 0.5 (center).
:type center_x: float
:param center_y: the center of the crop between 0 (top) and 1 (bottom)
Default to 0.5 (center).
:type center_y: float
:param int max_width: max width
:param int max_height: max height
:param float center_x: the center of the crop between 0 (left) and 1
(right). Defaults to 0.5 (center).
:param float center_y: the center of the crop between 0 (top) and 1
(bottom). Defaults to 0.5 (center).
:return: self to allow chaining
:rtype: ImageProcess
"""
@@ -332,25 +313,30 @@ def image_process(base64_source, size=(0, 0), verify_resolution=False, quality=0
def average_dominant_color(colors, mitigate=175, max_margin=140):
"""This function is used to calculate the dominant colors when given a list of colors
There are 5 steps :
1) Select dominant colors (highest count), isolate its values and remove
it from the current color set.
2) Set margins according to the prevalence of the dominant color.
3) Evaluate the colors. Similar colors are grouped in the dominant set
while others are put in the "remaining" list.
4) Calculate the average color for the dominant set. This is done by
averaging each band and joining them into a tuple.
5) Mitigate final average and convert it to hex
There are 5 steps:
1) Select dominant colors (highest count), isolate its values and remove
it from the current color set.
2) Set margins according to the prevalence of the dominant color.
3) Evaluate the colors. Similar colors are grouped in the dominant set
while others are put in the "remaining" list.
4) Calculate the average color for the dominant set. This is done by
averaging each band and joining them into a tuple.
5) Mitigate final average and convert it to hex
:param colors: list of tuples having:
[0] color count in the image
[1] actual color: tuple(R, G, B, A)
-> these can be extracted from a PIL image using image.getcolors()
0. color count in the image
1. actual color: tuple(R, G, B, A)
-> these can be extracted from a PIL image using
:meth:`~PIL.Image.Image.getcolors`
:param mitigate: maximum value a band can reach
:param max_margin: maximum difference from one of the dominant values
:returns: a tuple with two items:
[0] the average color of the dominant set as: tuple(R, G, B)
[1] list of remaining colors, used to evaluate subsequent dominant colors
0. the average color of the dominant set as: tuple(R, G, B)
1. list of remaining colors, used to evaluate subsequent dominant colors
"""
dominant_color = max(colors)
dominant_rgb = dominant_color[1][:3]
@@ -409,11 +395,10 @@ def image_fix_orientation(image):
save the complexity of removing it.
:param image: the source image
:type image: PIL.Image
:type image: ~PIL.Image.Image
:return: the resulting image, copy of the source, with orientation fixed
or the source image if no operation was applied
:rtype: PIL.Image
:rtype: ~PIL.Image.Image
"""
getexif = getattr(image, 'getexif', None) or getattr(image, '_getexif', None) # support PIL < 6.0
if getexif:
@@ -431,10 +416,7 @@ def base64_to_image(base64_source):
:param base64_source: the image base64 encoded
:type base64_source: string or bytes
:return: the PIL image
:rtype: PIL.Image
:rtype: ~PIL.Image.Image
:raise: UserError if the base64 is incorrect or the image can't be identified by PIL
"""
try:
@@ -446,12 +428,9 @@ def base64_to_image(base64_source):
def image_apply_opt(image, output_format, **params):
"""Return the given PIL `image` using `params`.
:param image: the PIL image
:type image: PIL.Image
:param params: params to expand when calling PIL.Image.save()
:type params: dict
:type image: ~PIL.Image.Image
:param str output_format: :meth:`~PIL.Image.Image.save`'s ``format`` parameter
:param dict params: params to expand when calling :meth:`~PIL.Image.Image.save`
:return: the image formatted
:rtype: bytes
"""
@@ -463,12 +442,9 @@ def image_apply_opt(image, output_format, **params):
def image_to_base64(image, output_format, **params):
"""Return a base64_image from the given PIL `image` using `params`.
:param image: the PIL image
:type image: PIL.Image
:param params: params to expand when calling PIL.Image.save()
:type params: dict
:type image: ~PIL.Image.Image
:param str output_format:
:param dict params: params to expand when calling :meth:`~PIL.Image.Image.save`
:return: the image base64 encoded
:rtype: bytes
"""
@@ -495,9 +471,7 @@ def image_guess_size_from_field_name(field_name):
If it can't be guessed, return (0, 0) instead.
:param field_name: the name of a field
:type field_name: string
:param str field_name: the name of a field
:return: the guessed size
:rtype: tuple (width, height)
"""
+4 -3
View File
@@ -2,7 +2,7 @@
This code is what let us use ES6-style modules in odoo.
Classic Odoo modules are composed of a top-level :samp:`odoo.define({name},{body_function})` call.
This processor will take files starting with an `@odoo-module` annotation (in a comment) and convert them to classic modules.
If any file has the /** odoo-module */ on top of it, it will get processed by this class.
If any file has the ``/** odoo-module */`` on top of it, it will get processed by this class.
It performs several operations to get from ES6 syntax to the usual odoo one with minimal changes.
This is done on the fly, this not a pre-processing tool.
@@ -560,9 +560,10 @@ def remove_index(content):
def relative_path_to_module_path(url, path_rel):
"""
Convert the relative path into a module path, which is more generic and fancy.
"""Convert the relative path into a module path, which is more generic and
fancy.
:param str url:
:param path_rel: a relative path to the current url.
:return: module path (@module/...)
"""
+25 -15
View File
@@ -358,16 +358,19 @@ def html2plaintext(html, body_id=None, encoding='utf-8'):
return html.strip()
def plaintext2html(text, container_tag=False):
""" Convert plaintext into html. Content of the text is escaped to manage
html entities, using misc.html_escape().
- all \n,\r are replaced by <br />
- enclose content into <p>
- convert url into clickable link
- 2 or more consecutive <br /> are considered as paragraph breaks
def plaintext2html(text, container_tag=None):
r"""Convert plaintext into html. Content of the text is escaped to manage
html entities, using :func:`~odoo.tools.misc.html_escape`.
:param string container_tag: container of the html; by default the
content is embedded into a <div>
- all ``\n``, ``\r`` are replaced by ``<br/>``
- enclose content into ``<p>``
- convert url into clickable link
- 2 or more consecutive ``<br/>`` are considered as paragraph breaks
:param str text: plaintext to convert
:param str container_tag: container of the html; by default the content is
embedded into a ``<div>``
:rtype: markupsafe.Markup
"""
text = misc.html_escape(ustr(text))
@@ -391,15 +394,18 @@ def plaintext2html(text, container_tag=False):
final = '<%s>%s</%s>' % (container_tag, final, container_tag)
return markupsafe.Markup(final)
def append_content_to_html(html, content, plaintext=True, preserve=False, container_tag=False):
def append_content_to_html(html, content, plaintext=True, preserve=False, container_tag=None):
""" Append extra content at the end of an HTML snippet, trying
to locate the end of the HTML document (</body>, </html>, or
EOF), and converting the provided content in html unless ``plaintext``
is False.
Content conversion can be done in two ways:
- wrapping it into a pre (preserve=True)
- use plaintext2html (preserve=False, using container_tag to wrap the
whole content)
- wrapping it into a pre (``preserve=True``)
- use plaintext2html (``preserve=False``, using ``container_tag`` to
wrap the whole content)
A side-effect of this method is to coerce all HTML tags to
lowercase in ``html``, and strip enclosing <html> or <body> tags in
content if ``plaintext`` is False.
@@ -410,6 +416,8 @@ def append_content_to_html(html, content, plaintext=True, preserve=False, contai
be wrapped in a <pre/> tag.
:param bool preserve: if content is plaintext, wrap it into a <pre>
instead of converting it into html
:param str container_tag: tag to wrap the content into, defaults to `div`.
:rtype: markupsafe.Markup
"""
html = ustr(html)
if plaintext and preserve:
@@ -513,7 +521,8 @@ def email_normalize(text):
def email_domain_extract(email):
""" Extract the company domain to be used by IAP services notably. Domain
is extracted from email information e.g:
- info@proximus.be -> proximus.be
- info@proximus.be -> proximus.be
"""
normalized_email = email_normalize(email)
if normalized_email:
@@ -530,7 +539,8 @@ def email_domain_normalize(domain):
def url_domain_extract(url):
""" Extract the company domain to be used by IAP services notably. Domain
is extracted from an URL e.g:
- www.info.proximus.be -> proximus.be
- www.info.proximus.be -> proximus.be
"""
parser_results = urlparse(url)
company_hostname = parser_results.hostname
+37 -41
View File
@@ -181,10 +181,10 @@ def file_open(name, mode="r", filter_ext=None):
Examples::
>>> file_open('hr/static/description/icon.png')
>>> file_open('hr/static/description/icon.png', filter_ext=('.png', '.jpg'))
>>> with file_open('/opt/odoo/addons/hr/static/description/icon.png', 'rb') as f:
... contents = f.read()
>>> file_open('hr/static/description/icon.png')
>>> file_open('hr/static/description/icon.png', filter_ext=('.png', '.jpg'))
>>> with file_open('/opt/odoo/addons/hr/static/description/icon.png', 'rb') as f:
... contents = f.read()
:param name: absolute or relative path to a file located inside an addon
:param mode: file open mode, as for `open()`
@@ -240,24 +240,25 @@ def reverse_enumerate(l):
"""Like enumerate but in the other direction
Usage::
>>> a = ['a', 'b', 'c']
>>> it = reverse_enumerate(a)
>>> it.next()
(2, 'c')
>>> it.next()
(1, 'b')
>>> it.next()
(0, 'a')
>>> it.next()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
>>> a = ['a', 'b', 'c']
>>> it = reverse_enumerate(a)
>>> it.next()
(2, 'c')
>>> it.next()
(1, 'b')
>>> it.next()
(0, 'a')
>>> it.next()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
"""
return zip(range(len(l)-1, -1, -1), reversed(l))
def partition(pred, elems):
""" Return a pair equivalent to:
``filter(pred, elems), filter(lambda x: not pred(x), elems)` """
``filter(pred, elems), filter(lambda x: not pred(x), elems)`` """
yes, nos = [], []
for elem in elems:
(yes if pred(elem) else nos).append(elem)
@@ -665,22 +666,6 @@ def split_every(n, iterable, piece_maker=tuple):
yield piece
piece = piece_maker(islice(iterator, n))
def get_and_group_by_field(cr, uid, obj, ids, field, context=None):
""" Read the values of ``field´´ for the given ``ids´´ and group ids by value.
:param string field: name of the field we want to read and group by
:return: mapping of field values to the list of ids that have it
:rtype: dict
"""
res = {}
for record in obj.read(cr, uid, ids, [field], context=context):
key = record[field]
res.setdefault(key[0] if isinstance(key, tuple) else key, []).append(record['id'])
return res
def get_and_group_by_company(cr, uid, obj, ids, context=None):
return get_and_group_by_field(cr, uid, obj, ids, field='company_id', context=context)
# port of python 2.6's attrgetter with support for dotted notation
def resolve_attr(obj, attr):
for name in attr.split("."):
@@ -769,7 +754,8 @@ class UnquoteEvalContext(defaultdict):
class mute_logger(logging.Handler):
"""Temporary suppress the logging.
Can be used as context manager or decorator.
Can be used as context manager or decorator::
@mute_logger('odoo.plic.ploc')
def do_stuff():
@@ -777,7 +763,6 @@ class mute_logger(logging.Handler):
with mute_logger('odoo.foo.bar'):
do_suff()
"""
def __init__(self, *loggers):
super().__init__()
@@ -978,7 +963,9 @@ def freehash(arg):
return id(arg)
def clean_context(context):
""" This function take a dictionary and remove each entry with its key starting with 'default_' """
""" This function take a dictionary and remove each entry with its key
starting with ``default_``
"""
return {k: v for k, v in context.items() if not k.startswith('default_')}
@@ -1118,6 +1105,8 @@ class Callbacks:
""" A simple queue of callback functions. Upon run, every function is
called (in addition order), and the queue is emptied.
::
callbacks = Callbacks()
# add foo
@@ -1142,6 +1131,8 @@ class Callbacks:
dictionary is automatically cleared by ``run()`` once all callback functions
have been called.
::
# register foo to process aggregated data
@callbacks.add
def foo():
@@ -1258,6 +1249,8 @@ def get_lang(env, lang_code=False):
Retrieve the first lang object installed, by checking the parameter lang_code,
the context and then the company. If no lang is installed from those variables,
fallback on the first lang installed in the system.
:param env:
:param str lang_code: the locale (i.e. en_US)
:return res.lang: the first lang found that is installed on the system.
"""
@@ -1367,10 +1360,12 @@ def parse_date(env, value, lang_code=False):
def format_datetime(env, value, tz=False, dt_format='medium', lang_code=False):
""" Formats the datetime in a given format.
:param {str, datetime} value: naive datetime to format either in string or in datetime
:param {str} tz: name of the timezone in which the given datetime should be localized
:param {str} dt_format: one of “full”, “long”, “medium”, or “short”, or a custom date/time pattern compatible with `babel` lib
:param {str} lang_code: ISO code of the language to use to render the given datetime
:param env:
:param str|datetime value: naive datetime to format either in string or in datetime
:param str tz: name of the timezone in which the given datetime should be localized
:param str dt_format: one of “full”, “long”, “medium”, or “short”, or a custom date/time pattern compatible with `babel` lib
:param str lang_code: ISO code of the language to use to render the given datetime
:rtype: str
"""
if not value:
return ''
@@ -1407,6 +1402,7 @@ def format_datetime(env, value, tz=False, dt_format='medium', lang_code=False):
def format_time(env, value, tz=False, time_format='medium', lang_code=False):
""" Format the given time (hour, minute and second) with the current user preference (language, format, ...)
:param env:
:param value: the time to format
:type value: `datetime.time` instance. Could be timezoned to display tzinfo according to format (e.i.: 'full' format)
:param tz: name of the timezone in which the given datetime should be localized
@@ -1604,7 +1600,7 @@ def get_diff(data_from, data_to, custom_style=False):
def traverse_containers(val, type_):
""" Yields atoms filtered by specified type_ (or type tuple), traverses
""" Yields atoms filtered by specified ``type_`` (or type tuple), traverses
through standard containers (non-string mappings or sequences) *unless*
they're selected by the type filter
"""
+7 -5
View File
@@ -124,7 +124,7 @@ def compute(function, seed=None):
as ``function(values, counter, random)``, where ``values`` is the other field values,
``counter`` is an integer, and ``random`` is a pseudo-random number generator.
:param function function: (values, counter, random) --> field_values
:param callable function: (values, counter, random) --> field_values
:param seed: optional initialization of the random number generator
:returns: function of the form (iterator, field_name, model_name) -> values
:rtype: function (iterator, str, str) -> dict
@@ -143,6 +143,7 @@ def randint(a, b, seed=None):
:param int a: minimal random value
:param int b: maximal random value
:param int seed:
:returns: function of the form (iterator, field_name, model_name) -> values
:rtype: function (iterator, str, str) -> dict
"""
@@ -163,12 +164,13 @@ def randdatetime(*, base_date=None, relative_before=None, relative_after=None, s
to a random datetime between relative_before and relative_after, relatively to
base_date
:param base_date (datetime): override the default base date if needed.
:param relative_after (relativedelta, timedelta): range up which we can go after the
:param datetime base_date: override the default base date if needed.
:param relativedelta|timedelta relative_after: range up which we can go after the
base date. If not set, defaults to 0, i.e. only in the past of reference.
:param relative_before (relativedelta, timedelta): range up which we can go before the
:param relativedelta|timedelta relative_before: range up which we can go before the
base date. If not set, defaults to 0, i.e. only in the future of reference.
:return (generator): iterator for random dates inside the defined range
:param seed:
:return: iterator for random dates inside the defined range
"""
base_date = base_date or datetime(2020, 1, 1)
seconds_before = relative_before and ((base_date + relative_before) - base_date).total_seconds() or 0
+1 -1
View File
@@ -254,7 +254,7 @@ def pg_varchar(size=0):
'infinite' VARCHAR
* Otherwise return a VARCHAR(n)
:type int size: varchar size, optional
:param int size: varchar size, optional
:rtype: str
"""
if size:
+5 -1
View File
@@ -76,9 +76,12 @@ def try_report_action(cr, uid, action_id, active_model=None, active_ids=None,
context=None, our_module=None):
"""Take an ir.actions.act_window and follow it until a report is produced
:param cr:
:param uid:
:param action_id: the integer id of an action, or a reference to xml id
of the act_window (can search [our_module.]+xml_id
:param active_model, active_ids: call the action as if it had been launched
:param active_model:
:param active_ids: call the action as if it had been launched
from that model+ids (tree/form view action)
:param wiz_data: a dictionary of values to use in the wizard, if needed.
They will override (or complete) the default values of the
@@ -86,6 +89,7 @@ def try_report_action(cr, uid, action_id, active_model=None, active_ids=None,
:param wiz_buttons: a list of button names, or button icon strings, which
should be preferred to press during the wizard.
Eg. 'OK' or 'fa-print'
:param context:
:param our_module: the name of the calling module (string), like 'account'
"""
if not our_module and isinstance(action_id, str):
+6 -2
View File
@@ -170,6 +170,7 @@ avoid_pattern = re.compile(r"\s*<!DOCTYPE", re.IGNORECASE | re.MULTILINE | re.UN
def translate_xml_node(node, callback, parse, serialize):
""" Return the translation of the given XML/HTML node.
:param node:
:param callback: callback(text) returns translated text or None
:param parse: parse(text) returns a node (text is unicode)
:param serialize: serialize(node) returns unicode text
@@ -1135,6 +1136,7 @@ def trans_load_data(cr, fileobj, fileformat, lang,
verbose=True, create_empty_translation=False, overwrite=False):
"""Populates the ir_translation table.
:param cr:
:param fileobj: buffer open to a translation file
:param fileformat: format of the `fielobj` file, one of 'po' or 'csv'
:param lang: language code of the translations contained in `fileobj`
@@ -1237,10 +1239,12 @@ def resetlocale():
def load_language(cr, lang):
""" Loads a translation terms for a language.
Used mainly to automate language loading at db initialization.
:param lang: language ISO code with optional _underscore_ and l10n flavor (ex: 'fr', 'fr_BE', but not 'fr-BE')
:type lang: str
:param cr:
:param str lang: language ISO code with optional underscore (``_``) and
l10n flavor (ex: 'fr', 'fr_BE', but not 'fr-BE')
"""
env = odoo.api.Environment(cr, odoo.SUPERUSER_ID, {})
installer = env['base.language.install'].create({'lang': lang})
+14 -11
View File
@@ -30,10 +30,11 @@ def _check_with_xsd(tree_or_str, stream, env=None):
This will raise a UserError if the XML file is not valid according to the
XSD file.
:param tree_or_str (etree, str): representation of the tree to be checked
:param stream (io.IOBase, str): the byte stream used to build the XSD schema.
:param str | etree._Element tree_or_str: representation of the tree to be checked
:param io.IOBase | str stream: the byte stream used to build the XSD schema.
If env is given, it can also be the name of an attachment in the filestore
:param env (odoo.api.Environment): If it is given, it enables resolving the
:param odoo.api.Environment env: If it is given, it enables resolving the
imports of the schema in the filestore with ir.attachments.
"""
if not isinstance(tree_or_str, etree._Element):
@@ -58,10 +59,12 @@ def create_xml_node_chain(first_parent_node, nodes_list, last_node_value=None):
Each new node being the child of the previous one based on the tags contained
in `nodes_list`, under the given node `first_parent_node`.
:param first_parent_node (etree._Element): parent of the created tree/chain
:param nodes_list (iterable<str>): tag names to be created
:param last_node_value (str): if specified, set the last node's text to this value
:returns (list<etree._Element>): the list of created nodes
:param etree._Element first_parent_node: parent of the created tree/chain
:param iterable[str] nodes_list: tag names to be created
:param str last_node_value: if specified, set the last node's text to this value
:returns: the list of created nodes
:rtype: list[etree._Element]
"""
res = []
current_node = first_parent_node
@@ -77,9 +80,9 @@ def create_xml_node_chain(first_parent_node, nodes_list, last_node_value=None):
def create_xml_node(parent_node, node_name, node_value=None):
"""Create a new node.
:param parent_node (etree._Element): parent of the created node
:param node_name (str): name of the created node
:param node_value (str): value of the created node (optional)
:returns (etree._Element):
:param etree._Element parent_node: parent of the created node
:param str node_name: name of the created node
:param str node_value: value of the created node (optional)
:rtype: etree._Element
"""
return create_xml_node_chain(parent_node, [node_name], node_value)[0]
+33
View File
@@ -45,3 +45,36 @@ requires =
python3-xlwt
python3-xlrd
python3-zeep
[flake8]
exclude =
.git,
.tx,
addons,
debian,
doc,
setup,
select =
RST
rst-directives =
function,
attribute,
seealso,
deprecated,
versionadded,
versionchanged,
todo
rst-roles =
ref,
mod,
class,
py:meth,
meth,
attr,
data,
const,
func,
exc,
term,
samp,
program