# coding=utf-8
# --------------------------------------------------------------------
# Copyright (C) 1991 - 2026 - EDF - www.code-aster.org
# This file is part of code_aster.
#
# code_aster is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# code_aster is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with code_aster. If not, see <http://www.gnu.org/licenses/>.
# --------------------------------------------------------------------
"""
This module defines the base objects that allow to use the legacy syntax
of code_aster commands.
Note:
- If a keyword (simple or factor) is not provided by the user, its value
is None.
- If a factor keyword is present by default (``statut='d'``) but not filled by
the user, its value is ``{}`` if ``max=1`` or ``[]`` if ``max > 1``.
- Only one level of factor keywords is supported.
The keyword ``reuse`` is (will be) deprecated. So it must be present in catalogs
to import command files that contain it. But it is always optional (see
``checkMandatory``) because it is removed during import.
"""
import types
from collections import OrderedDict
from . import DataStructure as DS
from .DataStructure import DataStructure, UnitBaseType
from .SyntaxChecker import CheckerError, checkCommandSyntax
from .SyntaxUtils import (
_F,
add_none_sdprod,
block_utils,
debug_message2,
disable_0key,
enable_0key,
force_list,
is_undefined,
sorted_dict,
value_is_sequence,
)
[docs]class SyntaxId:
"""Container of the id of syntax objects.
This list of type identifiers can be extended but never change between
two releases of code_aster.
"""
simp, fact, bloc, command = list(range(4))
IDS = SyntaxId()
UNDEF = object()
# Must stay identical to `AsterStudy.datamodel.general.ConversionLevel`
[docs]class ConversionLevel:
"""
Enumerator for the level of conversion requirements.
Attributes:
NoFail: Do not fail, not *strict*.
Naming: Requires that all command results are explicitly named.
Type: Requires a valid type definition.
Keyword: Requires that all keywords are valid.
Syntaxic: Requires a valid syntax of all the commands.
Restore: Requires a conversion without error during restore.
Any: All conversion must pass.
Partial: Allows to make a partial conversion (to be used with
another level).
NoGraphical: Force to load all stages in text mode.
"""
NoFail = 0x00
Naming = 0x01
Type = 0x02
Keyword = 0x04
Syntaxic = Naming | Type | Keyword
Restore = 0x08
Any = Syntaxic | Restore
Partial = 0x10
NoGraphical = 0x20
[docs]class CataDefinition(OrderedDict):
"""Dictionary to store the definition of syntax objects.
Iteration over the elements is ordered by type: SimpleKeyword, FactorKeyword
and Bloc.
"""
@property
def entities(self):
"""Return the all entities.
Returns:
dict: dict of all entities (keywords and conditional blocks) of the
object.
"""
kws = self._filter_entities((SimpleKeyword, Bloc, FactorKeyword), with_block=False)
return sorted_dict(kws)
@property
def keywords(self):
"""Return the simple and factor keywords contained in the object.
The keywords are sorted in the order of definition in the catalog.
This is a workaround waiting for Python 3.6 and integration
of `PEP-0468`_.
Returns:
dict: dict of all simple and factor keywords of the object.
.. _PEP-0468: https://www.python.org/dev/peps/pep-0468/
"""
kws = self._filter_entities((SimpleKeyword, FactorKeyword))
return sorted_dict(kws)
@property
def factor_keywords(self):
"""Return the factor keywords contained in the object.
Returns:
dict: dict of all factor keywords of the object.
"""
kws = self._filter_entities((FactorKeyword,))
return sorted_dict(kws)
@property
def simple_keywords(self):
"""Return the simple keywords contained in the object.
Returns:
dict: dict of all simple keywords of the object.
"""
kws = self._filter_entities((SimpleKeyword,))
return sorted_dict(kws)
[docs] def _filter_entities(self, typeslist, with_block=True):
"""Filter entities by type recursively.
Returns:
dict: dict of entities of the requested types.
"""
entities = CataDefinition()
for key, value in list(self.items()):
if isinstance(value, typeslist):
entities[key] = value
elif with_block and isinstance(value, Bloc):
bloc_kwds = value.definition._filter_entities(typeslist)
# update without overwriting factorkeywords but extending them
for kwd, obj in bloc_kwds.items():
if type(obj) is FactorKeyword and kwd in entities:
# do not change existing FactorKeyword!
all_kwds = entities[kwd].definition.copy()
all_kwds.update(obj.definition)
entities[kwd] = FactorKeyword(all_kwds)
else:
entities[kwd] = obj
return entities
[docs] def iterItemsByType(self):
"""Iterator over dictionary's pairs with respecting order:
SimpleKeyword, FactorKeyword and Bloc objects"""
keysR = [k for k, v in list(self.items()) if type(v) is SimpleKeyword]
keysI = [k for k, v in list(self.items()) if type(v) is FactorKeyword]
keysS = [k for k, v in list(self.items()) if type(v) is Bloc]
for key in keysR + keysI + keysS:
yield (key, self[key])
[docs]class UIDMixing:
"""Sub class for UID based classes.
Arguments:
uid (int): Object's id.
"""
_new_id = -1
_id = None
@classmethod
def new_id(cls):
UIDMixing._new_id += 1
return UIDMixing._new_id
[docs] def __init__(self):
self._id = self.new_id()
@property
def uid(self):
"""Attribute that holds unique *id*"""
return self._id
def __lt__(self, other):
if other is None or not hasattr(other, "uid"):
return True
return self._id < other.uid
def __eq__(self, other):
if other is None or not hasattr(other, "uid"):
return False
return self._id == other.uid
[docs]class PartOfSyntax(UIDMixing):
"""
Generic object that describe a piece of syntax.
"""
[docs] def __init__(self, curDict):
"""Initialization"""
super().__init__()
self._definition = CataDefinition(curDict)
regles = curDict.get("regles")
if regles and type(regles) not in (list, tuple):
regles = (regles,)
self._rules = regles or []
[docs] def getCataTypeId(self):
"""Get the Cata type of object.
Should be sub-classed.
Returns:
int: type id of Cata object: -1 if not defined.
"""
return -1
@property
def name(self):
"""str: Name of the object."""
return self._definition.get("nom", "")
@property
def udocstring(self):
"""unicode: Documentation of the object."""
doc = self.docstring
if type(doc) is not str:
doc = str(doc, "utf-8", "replace")
return doc
@property
def docstring(self):
"""str: Documentation of the object."""
return self._definition.get("fr", "")
@property
def definition(self):
"""dict: Attribute containing the syntax definition"""
return self._definition
@property
def rules(self):
"""dict: Attribute containing the list of rules"""
return self._rules
# : for backward compatibility
regles = rules
[docs] def __repr__(self):
"""Simple representation"""
return "%s( %r )" % (self.__class__, self._definition)
@property
def entities(self):
"""Return the all entities contained in the object.
Returns:
dict: dict of all entities (keywords and conditional blocks) of the
object.
"""
return self._definition.entities
# : for backward compatibility (and avoid changing `pre_seisme_nonl`)
entites = entities
@property
def keywords(self):
"""Return the simple and factor keywords contained in the object.
Returns:
dict: dict of all simple and factor keywords of the object.
"""
return self._definition.keywords
[docs] def accept(self, visitor, syntax=None):
"""Called by a Visitor"""
raise NotImplementedError("must be defined in a subclass")
[docs] def _def_status(self):
"""Wrapper that returns the value of 'statut' after precondition."""
definition = self.definition
value = definition.get("statut", "f")
if value == "c":
return value
# In AsterStudy, a simple keyword with a default value...
if self.getCataTypeId() == IDS.simp and self.hasDefaultValue():
try:
# ... with derivated type of UnitBaseType...
if issubclass(definition.get("typ"), UnitBaseType):
# ... is mandatory
return "o"
except TypeError:
pass
return value
[docs] def isMandatory(self):
"""Tell if this keyword is mandatory"""
return self._def_status() == "o"
[docs] def isOptional(self):
"""Tell if this keyword is optional"""
return self._def_status() == "f"
[docs] def isHidden(self):
"""Tell if this keyword should be hidden"""
return self._def_status() == "c"
[docs] def hasDefaultValue(self):
"""Tell if the keyword has a default value"""
return False
[docs] def addDefaultKeywords(self, userSyntax, _parent_ctxt=None):
"""Add default keywords into the user dict of keywords.
Optional keywords that are not defined are set to None/undefined value.
The values given in argument (userSyntax) preempt on the definition
ones.
Arguments:
userSyntax (dict): dict of the keywords as filled by the user,
**changed** in place.
_parent_ctxt (dict): contains the keywords as known in the parent.
This context is used to evaluate block conditions.
"""
if userSyntax is None:
return
ctxt = _parent_ctxt.copy() if _parent_ctxt else {}
for key, kwd in self.definition.iterItemsByType():
if isinstance(kwd, SimpleKeyword):
# use the default
kwd.addDefaultKeywords(key, userSyntax, ctxt)
elif isinstance(kwd, FactorKeyword):
# if not given by the user, default value is None if optional
# or {} if present by default
userFact = userSyntax.get(key, kwd.defaultValue())
if userFact is None:
pass
elif type(userFact) in (list, tuple):
for userOcc in userFact:
try:
kwd.addDefaultKeywords(userOcc, ctxt)
except AttributeError:
raise CheckerError(
TypeError, f"{key}: Unexpected type: {type(userOcc)}", []
)
else:
try:
kwd.addDefaultKeywords(userFact, ctxt)
except AttributeError:
raise CheckerError(
TypeError, f"{key}: Unexpected type: {type(userFact)}", []
)
if kwd.is_list():
userFact = [userFact]
userSyntax[key] = userFact
ctxt[key] = userSyntax[key]
elif isinstance(kwd, Bloc):
if kwd.isEnabled(ctxt):
kwd.addDefaultKeywords(userSyntax, ctxt)
[docs] def checkMandatory(self, userSyntax, stack, _parent_ctxt=None):
"""Check that the mandatory keywords are provided by the user.
Warning: Default keywords must be added before visiting the objects.
Warning: this does not check recursively, only the current level.
Arguments:
userSyntax (dict): dict of the user and default keywords.
stack (list): used to give contextual informations in error
messages.
_parent_ctxt (dict): contains the keywords as known in the parent.
This context is used to evaluate block conditions.
"""
ctxt = _parent_ctxt.copy() if _parent_ctxt else {}
# update parent context with local keywords
ctxt.update(userSyntax)
for key, kwd in self.definition.iterItemsByType():
if isinstance(kwd, (SimpleKeyword, FactorKeyword)):
if key == "reuse": # reuse is deprecated
continue
# raise KeyError("reuse has been removed")
# pragma pylint: disable=no-member
if kwd.isMandatory() and kwd.undefined(userSyntax.get(key)):
debug_message2("mandatory keyword =", key, ":", kwd)
debug_message2("given syntax =", userSyntax)
stack.append(key)
raise KeyError("Keyword {0} is mandatory".format(key))
elif isinstance(kwd, Bloc):
if kwd.isEnabled(ctxt):
kwd.checkMandatory(userSyntax, stack, ctxt)
# else: sdprod, fr...
[docs] def getKeyword(self, userKeyword, userSyntax, _parent_ctxt=None):
"""Return the keyword in the current composite object.
Warning: Default keywords must be added before visiting the objects.
Arguments:
userKeyword (str): name of the searched keyword.
userSyntax (dict): dict of the user and default keywords.
_parent_ctxt (dict): contains the keywords as known in the parent.
This context is used to evaluate block conditions.
"""
# search in keywords list
found = self.definition.get(userKeyword)
if found:
return found
ctxt = _parent_ctxt.copy() if _parent_ctxt else {}
# update parent context with local keywords
ctxt.update(userSyntax)
# search in BLOC objects
for _, kwd in self.definition.iterItemsByType():
if not isinstance(kwd, Bloc):
continue
# debug_message2("block", key, repr(kwd.getCondition()), "with", ctxt,
# "enabled ?", kwd.isEnabled(ctxt))
if not kwd.isEnabled(ctxt):
continue
found = kwd.getKeyword(userKeyword, userSyntax, ctxt)
if found:
break
return found
[docs] def getRules(self, userSyntax, _parent_ctxt=None):
"""Return the rules to be applied to the given keywords.
Warning: Default keywords must be added before visiting the objects.
Arguments:
userSyntax (dict): dict of the user and default keywords.
_parent_ctxt (dict): contains the keywords as known in the parent.
This context is used to evaluate block conditions.
"""
ctxt = _parent_ctxt.copy() if _parent_ctxt else {}
# update parent context with local keywords
ctxt.update(userSyntax)
rules = list(self.rules)
# search in BLOC objects
for _, kwd in self.definition.iterItemsByType():
if not isinstance(kwd, Bloc):
continue
if not kwd.isEnabled(ctxt):
continue
rules.extend(kwd.getRules(userSyntax))
return rules
[docs] @classmethod
def undefined(cls, value):
"""Return *True* if the value is a null value (undefined keyword),
*False* otherwise."""
return is_undefined(value)
[docs] def is_list(self):
"""Tell if the value should be stored as list."""
return self.definition.get("max", 1) != 1
[docs]class SimpleKeyword(PartOfSyntax):
"""
Objet mot-clé simple équivalent à SIMP dans les capy
"""
[docs] def __init__(self, curDict):
"""Initialization"""
super(SimpleKeyword, self).__init__(curDict)
if "val_min" in self._definition or "val_max" in self._definition:
typ = self._definition["typ"]
assert typ in ("I", "R", "C") or issubclass(
typ, UnitBaseType
), "'val_min/val_max' not allowed for type" " '{0}'".format(typ)
[docs] def getCataTypeId(self):
"""Get the type id of SimpleKeyword.
Returns:
int: type id of SimpleKeyword.
"""
return IDS.simp
[docs] def accept(self, visitor, syntax=None):
"""Called by a Visitor"""
visitor.visitSimpleKeyword(self, syntax)
[docs] def _context(self, value):
"""Print contextual informations"""
debug_message2("CONTEXT: value={!r}, type={}".format(value, type(value)))
debug_message2("CONTEXT: definition: {}".format(self))
[docs] def hasDefaultValue(self):
"""Tell if the keyword has a default value"""
return self.defaultValue(UNDEF) is not UNDEF
[docs] def defaultValue(self, default=None):
"""Return the default value or *default*."""
return self.definition.get("defaut", default)
[docs] def addDefaultKeywords(self, key, userSyntax, _parent_ctxt):
"""Add the default value if not provided by the user dict.
Arguments:
userSyntax (dict): dict of the keywords as filled by the user,
**changed** in place.
_parent_ctxt (dict): contains the keywords as known in the parent.
"""
value = userSyntax.get(key, self.defaultValue())
# do not insert None value
if value is None:
return
if self.is_list():
if value is not None and not value_is_sequence(value):
value = [value]
userSyntax[key] = value
_parent_ctxt[key] = userSyntax[key]
[docs]class FactorKeyword(PartOfSyntax):
"""
Objet mot-clé facteur equivalent de FACT dans les capy
"""
[docs] def getCataTypeId(self):
"""Get the type id of FactorKeyword.
Returns:
int: type id of FactorKeyword.
"""
return IDS.fact
[docs] def accept(self, visitor, syntax=None):
"""Called by a Visitor"""
visitor.visitFactorKeyword(self, syntax)
[docs] def defaultValue(self):
"""Return the *default* for this factor keyword.
If the keyword is present by default, its default value is {} if max=1
or [] if max > 1.
If the keyword does not exist by default, its default value is None."""
if self._def_status() not in ("c", "d"):
return None
if self.is_list():
return [_F()]
return _F()
[docs]class Bloc(PartOfSyntax):
"""
Objet Bloc équivalent à BLOC dans les capy
"""
[docs] def getCataTypeId(self):
"""Get the type id of Bloc.
Returns:
int: type id of Bloc.
"""
return IDS.bloc
[docs] def accept(self, visitor, syntax=None):
"""Called by a Visitor"""
visitor.visitBloc(self, syntax)
[docs] def getCondition(self):
"""Return the BLOC condition"""
cond = self.definition.get("condition")
assert cond is not None, "A bloc must have a condition!"
return cond
[docs] def isEnabled(self, context):
"""Tell if the block is enabled by the given context"""
eval_context = {}
eval_context.update(DS.__dict__)
eval_context.update(block_utils(eval_context))
# evaluate Python variables if present
for key, value in list(context.items()):
eval_context[key] = getattr(value, "evaluation", value)
try:
enabled = eval(self.getCondition(), {}, eval_context)
except AssertionError:
raise
except Exception:
# TODO: re-enable CataError, it seems me a catalog error!
# raise CataError("Error evaluating {0!r}: {1}".format(
# self.getCondition(), str(exc)))
enabled = False
return enabled
[docs]class Command(PartOfSyntax):
"""
Object Command qui représente toute la syntaxe d'une commande
"""
_call_callback = None
[docs] def get_compat_syntax(self):
"""Return the `compat_syntax` keywords converter."""
return self.definition.get("compat_syntax", lambda _: None)
[docs] @classmethod
def register_call_callback(cls, callback):
"""Register *callback* to be called in place of the default method.
Register *None* to revert to default.
Arguments:
callback (callable): Function to call with signature:
(*Command* instance, ``**kwargs``).
"""
cls._call_callback = callback
[docs] def getCataTypeId(self):
"""Get the type id of Command.
Returns:
int: type id of Command.
"""
return IDS.command
[docs] def can_reuse(self):
"""Tell if the result can be a reused one."""
reentr = self.definition.get("reentrant", "").split(":")
return reentr and reentr[0] in ("o", "f")
[docs] def accept(self, visitor, syntax=None):
"""Called by a Visitor"""
visitor.visitCommand(self, syntax)
[docs] def call_default(self, **args):
"""Execute the command, only based on the command description.
Keyword arguments:
__strict__ (bool): If True, the syntax of the command must be valid.
Otherwise the generic DataStructure type is returned.
"""
strict = args.pop("__strict__", ConversionLevel.Syntaxic)
if strict & ConversionLevel.Syntaxic:
checkCommandSyntax(self, args)
resultType = self.get_type_sd_prod(**args)
else:
try:
resultType = self.get_type_sd_prod(**args)
except:
return DataStructure()
if resultType is None:
return None
return resultType()
[docs] def __call__(self, **args):
"""Simulate the command execution."""
if Command._call_callback is None:
return self.call_default(**args)
else:
return Command._call_callback(self, **args)
[docs] def get_type_sd_prod(self, **ctxt):
"""Return the type of the command result."""
resultType = self.definition.get("sd_prod")
if resultType is not None:
if type(resultType) is types.FunctionType:
self.addDefaultKeywords(ctxt)
# if os.environ.get('DEBUG'):
# print "COMMAND:", self.name
# print "CTX1:", ctxt.keys()
# print "CTX1:", ctxt
add_none_sdprod(resultType, ctxt)
resultType = self.build_sd_prod(resultType, ctxt)
return resultType
[docs] def build_sd_prod(self, sdprodFunc, ctxt):
"""Call the `sd_prod` function"""
enable_0key(ctxt)
resultType = sdprodFunc(**ctxt)
disable_0key(ctxt)
return resultType
[docs] def get_all_types(self):
"""Return the list of all possible types that the command can return.
Returns:
list[.DataStructure.DataStructure] or list[list[.DataStructure.DataStructure]]:
List of possible
types or a list of list if there are additional results for
macro-commands.
"""
sd_prod = self.definition.get("sd_prod")
if type(sd_prod) is types.FunctionType:
args = {}
add_none_sdprod(sd_prod, args)
args["__all__"] = True
return force_list(sd_prod(**args))
else:
return (sd_prod,)
[docs]class Operator(Command):
pass
[docs]class Macro(Command):
"""Specialization of the Command object for MACRO"""
[docs] def build_sd_prod(self, sdprodFunc, ctxt):
"""Call the `sd_prod` function"""
enable_0key(ctxt)
if "self" in ctxt:
del ctxt["self"]
resultType = sdprodFunc(self, **ctxt)
disable_0key(ctxt)
return resultType
[docs] def type_sdprod(self, result, astype):
"""Define the type of the result."""
if result is None:
return
result.settype(astype)
[docs] def accept(self, visitor, syntax=None):
"""Called by a Visitor"""
visitor.visitMacro(self, syntax)
[docs]class Procedure(Command):
pass
[docs]class CataError(Exception):
"""Exception raised in case of error in the catalog."""
pass