import contextlib
import io
import logging
import pathlib
import re
import textwrap
import sys
from .tecutil import _tecutil, _tecutil_connector
from .exception import TecplotLogicError, TecplotSystemError, TecplotMacroError
from . import tecutil
log = logging.getLogger(__name__)
if not hasattr(contextlib, 'nullcontext'):
@contextlib.contextmanager
def nullcontext(result=None):
yield result
contextlib.nullcontext = nullcontext
[docs]
@tecutil.lock()
def execute_function(name, parameters=None):
"""Runs a macro function.
Parameters:
name: (`str`): Name of the macro function to run. This name
is not case sensitive. Must be a non-zero length `str`.
parameters: (`str`): Any parameters which the quick macro
requires. Pass `None` (default) for macro functions which require no
parameters. The parameters are passed as a string which includes
the parenthesis. For example, if the macro function takes a
string parameter and int parameter, pass
**'("string_parameter_name", 2)'** Default: `None`
Macro functions must be defined before they are used. Typically they
are defined in the 'tecplot.mcr' configuration file which is read
when |Tecplot Engine| is initialized. Macro functions
may also be available in the Quick Macro Panel when the Tecplot 360 GUI
is running.
.. note::
See the |Tecplot Macro Scripting Guide| for more information about
macro functions.
Run the "Tile Frames" macro functions. This macro function is defined in
the file "tecplot.mcr", which is located in the Tecplot 360
installation directory::
>>> tecplot.macro.execute_function('Tile Frames')
Run a macro function named "Calculate" which takes no parameters and
another macro function called "Display" which takes the name of a
layout file and an integer as parameters::
>>> tp.macro.execute_function('Calculate')
>>> tp.macro.execute_function('Display', '("contour.lay", 2)')
"""
try:
if not _tecutil.MacroRunFunction(
name.strip(),
parameters.strip() if parameters else None):
raise TecplotMacroError(name)
except (TecplotLogicError, TecplotSystemError) as e:
raise TecplotMacroError(str(e))
[docs]
@tecutil.lock()
def execute_command(command):
"""Runs a series of tecplot macro commands.
Parameters:
command (`str`): The macro commands to be run.
.. warning:: Zero-based Indexing
It is important to know that all indexing in |PyTecplot| scripts are
zero-based. This is a departure from the macro language which is
one-based. This is to keep with the expectations when working in the
python language. However, |PyTecplot| does not modify strings that are
passed to the |Tecplot Engine|. This means that one-based indexing
should be used when running macro commands from python or when using
`execute_equation() <tecplot.data.operate.execute_equation>`.
This command splits the input into individual commands and runs them one
at a time. See the |Tecplot Macro Scripting Guide| for details about
|Tecplot 360|'s macro language.
.. warning::
The $!VARSET command is not supported. Tecplot Macro variables should be
converted to Python variables.
.. warning::
Intrinsic variables (that is, variables with pipes such as
``|DATASETFNAME|``) are not supported. If you need to use an intrinsic
variable in the macro command, add the macro command to a text file and
call `execute_file`.
See the |Tecplot Macro Scripting Guide| for more information about raw data
and intrinsic variables.
The following command will perform the same operations as the
`Hello, World! example <hello_world>`::
>>> tecplot.macro.execute_command(r'''
... $!ATTACHTEXT
... ANCHORPOS { X = 35 Y = 50 }
... TEXTSHAPE { HEIGHT = 35 }
... TEXT = 'Hello, World!'
... $!EXPORTSETUP EXPORTFNAME = 'hello_world.png'
... $!EXPORT
... EXPORTREGION = CURRENTFRAME
... ''')
"""
varset = re.compile(r'\$!VARSET.*', re.IGNORECASE)
for c in tecutil.split_macro(command):
if __debug__:
if varset.match(c):
msg = ('The $!VARSET command is not supported in\n'
'execute_macro(). Python variables should be used\n'
'instead of macro variables. Alternatively, you can\n'
'execute a macro in a file with '
'macro.execute_file().\n'
'$!VARSET is supported in execute_file()')
raise TecplotMacroError(c + '\n' + msg)
log.debug('executing command:\n' + c)
try:
if not _tecutil.MacroExecuteCommand(c):
raise TecplotMacroError(c)
except (TecplotLogicError, TecplotSystemError) as e:
raise TecplotMacroError(str(e))
[docs]
@tecutil.lock()
def execute_extended_command(command_processor_id, command, raw_data=None):
"""Runs a tecplot macro command defined in an addon.
Parameters:
command_processor_id (`str`): A unique string used to
determine the API to call when an extended macro command is
processed. API's are provided by add-ons or applications
that extend the Tecplot macro language.
Typically this will be the name of an add-on or application,
followed by a version number. For example: 'CFDAnalyzer4'.
Each application or add-on may provide one or more unique
command processor ID strings corresponding to different API's,
or different versions of an API.
For example, a file converter add-on responsible
for converting DXF files for Tecplot might provide two versions
of an API:
"DXFCONVERTTOOL-1.2", and "DXFCONVERTTOOL-2.0".
In that case either of these strings would be passed in the
*command_processor_id* parameter to indicate the version of the
API to use.
command (`str`): The command to run.
raw_data (`str`): Raw data required for the command, if any
(default: `None`).
.. warning:: Zero-based Indexing
It is important to know that all indexing in |PyTecplot| scripts are
zero-based. This is a departure from the macro language which is
one-based. This is to keep with the expectations when working in the
python language. However, |PyTecplot| does not modify strings that are
passed to the |Tecplot Engine|. This means that one-based indexing
should be used when running macro commands from python or when using
`execute_equation() <tecplot.data.operate.execute_equation>`.
In general, the command string is formatted prior to being fed into the
|Tecplot Engine| so liberal use of whitespace, including new-lines, are
acceptable.
Example::
>>> tecplot.macro.execute_extended_command(
... 'Multi Frame Manager',
... 'TILEFRAMESSQUARE')
"""
try:
if not _tecutil.MacroExecuteExtendedCommand(
command_processor_id, command.replace('\n', ' '), raw_data):
raise TecplotMacroError(command)
except AttributeError:
command = textwrap.dedent('''
$!EXTENDEDCOMMAND
COMMANDPROCESSORID = '{procid}'
COMMAND = '{cmd}'
'''.format(procid=command_processor_id,
cmd=' '.join(command. split()).replace(r"'", r"\'")))
execute_command(command)
except TecplotLogicError:
raise TecplotMacroError()
[docs]
@tecutil.lock()
def execute_file(filename):
"""Run a macro file.
Parameters:
filename (`pathlib.Path` or `str`): The file to be run. (See note below
concerning absolute and relative paths.)
.. warning:: Zero-based Indexing
It is important to know that all indexing in |PyTecplot| scripts are
zero-based. This is a departure from the macro language which is
one-based. This is to keep with the expectations when working in the
python language. However, |PyTecplot| does not modify strings that are
passed to the |Tecplot Engine|. This means that one-based indexing
should be used when running macro commands from python or when using
`execute_equation() <tecplot.data.operate.execute_equation>`.
.. note:: **Absolute and relative paths with PyTecplot**
Relative paths, when used within the PyTecplot API are always from
Python's current working directory which can be obtained by calling
:func:`os.getcwd()`. This is true for batch and `connected
<tecplot.session.connect()>` modes. One exception to this is paths
within a macro command or file which will be relative to the |Tecplot
Engine|'s home directory, which is typically the |Tecplot 360|
installation directory. Finally, when connected to a remote (non-local)
instance of Tecplot 360, only absolute paths are allowed.
Note that backslashes must be escaped which is especially important for
windows paths such as ``"C:\\\\Users"`` or ``"\\\\\\\\server\\\\path"``
which will resolve to ``"C:\\Users"`` and ``"\\\\server\\path"``
respectively. Alternatively, one may use Python's raw strings:
``r"C:\\Users"`` and ``r"\\\\server\\path"``
Example::
>>> tecplot.macro.execute_file('/path/to/macro_file.mcr')
"""
try:
filepath = tecutil.normalize_path(filename)
if not _tecutil.MacroRunFile(str(filepath)):
raise TecplotMacroError(filepath)
except (TecplotLogicError, TecplotSystemError) as e:
raise TecplotMacroError(str(e))
@contextlib.contextmanager
def record(out=None, header=None):
"""Record a block of python code as a macro.
Parameters:
out (`pathlib.Path` or `str` filename, output stream object or `None`): Output
file name of the resulting macro or a text buffer object that implements
write() and flush() methods. If `None`, then a `io.StringIO()` object is
yielded and can be used to get the string after exiting the context.
header (`str`, optional): String to prepend to the output. This will
default to "#!MC 1410\n" for a macro file, but be empty when output
to a buffer. Set this to an empty string to elide the header when
writing to a file.
Example output to a file::
import tecplot as tp
with tp.macro.record('recording.mcr'):
tp.active_frame().plot().show_contour = True
The resulting contents of the recording.mcr::
$!FieldLayers ShowContour = Yes
Example output to a string buffer::
import io
import tecplot as tp
with io.StringIO() as buf:
with tp.macro.record(buf):
tp.active_frame().plot().show_contour = True
mcr = buf.getvalue()
print(mcr)
Example output to a new string buffer (setting out to `None`). This
will produce the same string as the string buffer example above::
import tecplot as tp
with tp.macro.record() as buf:
tp.active_frame().plot().show_contour = True
mcr = buf.getvalue()
print(mcr)
"""
suffix = '.mcr'
if isinstance(out, (pathlib.Path, str)):
if str(out).endswith('.py'):
suffix = '.py'
ctx = contextlib.closing(open(out, 'w'))
header = '#!MC 1410\n' if header is None else header
elif out is None:
ctx = contextlib.nullcontext(io.StringIO())
else:
ctx = contextlib.nullcontext(out)
with ctx as ostream:
with tecutil.temporary_closed_file(suffix=suffix) as ftmp:
_tecutil_connector.macro_record_start(ftmp)
try:
with tecutil.force_recording():
yield ostream
finally:
_tecutil_connector.macro_record_end()
with open(ftmp, 'r') as fin:
src = fin.read().strip()
if header is not None:
ostream.write(header)
if suffix == '.mcr':
src = src[src.find('$!'):]
ostream.write(src)
ostream.flush()
def translate(source):
"""Translate macro source code into Python.
Parameters:
source (`str`): A multiline string containing the macro commands to be
translated.
If no translation is available or the translation fails, the
`tp.macro.execute_command()` method will be used as a fall-back. No import
statements are included, and the following header is assumed::
import tecplot as tp
from tecplot.constant import *
"""
result = []
for command in tecutil.split_macro(source):
result.append(_tecutil_connector.translate_macro_to_python(command))
return '\n'.join(result)