• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

SDXorg / pysd / 18087736285

26 Aug 2025 09:06AM UTC coverage: 98.751% (-1.1%) from 99.861%
18087736285

push

github

enekomartinmartinez
Bump lycheeverse/lychee-action from 2.5.0 to 2.6.1

Bumps [lycheeverse/lychee-action](https://github.com/lycheeverse/lychee-action) from 2.5.0 to 2.6.1.
- [Release notes](https://github.com/lycheeverse/lychee-action/releases)
- [Commits](https://github.com/lycheeverse/lychee-action/compare/v2.5.0...v2.6.1)

---
updated-dependencies:
- dependency-name: lycheeverse/lychee-action
  dependency-version: 2.6.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

4981 of 5044 relevant lines covered (98.75%)

0.99 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

71.91
/pysd/cli/parser.py
1
"""
2
cmdline parser
3
"""
4
import os
1✔
5
import re
1✔
6
from ast import literal_eval
1✔
7
import numpy as np
1✔
8
import pandas as pd
1✔
9
from argparse import ArgumentParser, Action
1✔
10

11
from pysd import __version__
1✔
12
from pysd.translators.vensim.vensim_utils import supported_extensions as\
1✔
13
    vensim_extensions
14
from pysd.translators.xmile.xmile_utils import supported_extensions as\
1✔
15
    xmile_extensions
16

17
docs = "https://pysd.readthedocs.io/en/master/command_line_usage.html"
1✔
18

19
parser = ArgumentParser(
1✔
20
    description='Simulating System Dynamics Models in Python',
21
    prog='PySD')
22

23

24
#########################
25
# functions and actions #
26
#########################
27

28
def check_output(string):
1✔
29
    """
30
    Checks that out put file ends with .tab or .csv
31

32
    """
33
    if not string.endswith(('.tab', '.csv', '.nc')):
×
34
        parser.error(
×
35
            f'when parsing {string}'
36
            '\nThe output file name must be .tab, .csv or .nc...')
37

38
    return string
×
39

40

41
def check_model(string):
1✔
42
    """
43
    Checks that model file ends with .py .mdl or .xmile and that exists.
44

45
    """
46
    suffixes = [".py"] + vensim_extensions + xmile_extensions
1✔
47
    if not any(string.lower().endswith(suffix) for suffix in suffixes):
1✔
48
        parser.error(
1✔
49
            f"when parsing {string} \nThe model file name must be a Vensim"
50
            f" ({', '.join(vensim_extensions)}), a Xmile "
51
            f"({', '.join(xmile_extensions)}) or a PySD (.py) model file...")
52

53
    if not os.path.isfile(string):
1✔
54
        parser.error(
×
55
            f"when parsing {string}"
56
            "\nThe model file does not exist...")
57

58
    return string
1✔
59

60

61
def check_data_file(string):
1✔
62
    """
63
    Check that data file is a tab or csv file and that exists.
64
    """
65
    if not string.endswith(('.tab', '.csv', '.nc')):
×
66
        parser.error(
×
67
            f'when parsing {string}'
68
            '\nThe data file name must be .tab, .csv or .nc...')
69
    elif not os.path.isfile(string):
×
70
        parser.error(
×
71
            f'when parsing {string}'
72
            '\nThe data file does not exist...')
73
    else:
74
        return string
×
75

76

77
def split_files(string):
1✔
78
    """
79
    Splits the data files and returns and error if file doesn't exists
80
    --data 'file1.tab, file2.csv' -> ['file1.tab', 'file2.csv']
81
    --data file1.tab -> ['file1.tab']
82

83
    """
84
    return [check_data_file(s.strip()) for s in string.split(',')]
×
85

86

87
def split_columns(string):
1✔
88
    """
89
    Splits the return-columns argument or reads it from .txt
90
    --return-columns 'temperature c, "heat$"' -> ['temperature c', '"heat$"']
91
    --return-columns my_vars.txt -> ['temperature c', '"heat$"']
92

93
    """
94
    if string.endswith('.txt'):
×
95
        with open(string, 'r') as file:
×
96
            return [col.rstrip('\n').strip() for col in file]
×
97

98
    return [s.strip() for s in string.split(',')]
×
99

100

101
def split_timestamps(string):
1✔
102
    """
103
    Splits the return-timestamps argument
104
    --return-timestamps '1, 5, 15' -> array([1., 5., 15.])
105

106
    """
107
    try:
×
108
        return np.array([s.strip() for s in string.split(',')], dtype=float)
×
109
    except Exception:
×
110
        # error
111
        raise parser.error(
×
112
            f'when parsing {string}'
113
            '\nThe return time stamps must be separated by commas...\n'
114
            f'See {docs} for examples.')
115

116

117
def split_vars(string):
1✔
118
    """
119
    Splits the arguments from new_values.
120
    'a=5' -> {'a': ('param', 5.)}
121
    'b=[[1,2],[1,10]]' -> {'b': ('param', pd.Series(index=[1,2], data=[1,10]))}
122
    'a:5' -> {'a': ('initial', 5.)}
123

124
    """
125
    try:
1✔
126
        if '=' in string:
1✔
127
            # new variable value
128
            var, value = string.split('=')
1✔
129
            type = 'param'
1✔
130

131
        if ':' in string:
1✔
132
            # initial time value
133
            var, value = string.split(':')
1✔
134
            type = 'initial'
1✔
135

136
        if re.match(r"^[+-]?(\d*\.)?\d+$", value.strip()):
1✔
137
            # value is a number
138
            return {var.strip(): (type, float(value))}
1✔
139

140
        # value is series
141
        assert type == 'param'
×
142
        value = literal_eval(value)
×
143
        assert len(value) == 2
×
144
        assert len(value[0]) == len(value[1])
×
145
        return {var.strip(): (type,
×
146
                              pd.Series(index=value[0], data=value[1]))}
147

148
    except Exception:
×
149
        # error
150
        raise parser.error(
×
151
            f'when parsing {string}'
152
            '\nYou must use variable=new_value to redefine values or '
153
            'variable:initial_value to define initial value.'
154
            'variable must be a model component, new_value can be a '
155
            'float or a list of two list, initial_value must be a float'
156
            '...\n'
157
            f'See {docs} for examples.')
158

159

160
class SplitVarsAction(Action):
1✔
161
    """
162
    Convert the list of split variables from new_values to a dictionary.
163
    [{'a': 5.}, {'b': pd.Series(index=[1, 2], data=[1, 10])}] ->
164
        {'a': 5., 'b': pd.Series(index=[1, 2], data=[1, 10])}
165
    """
166
    def __init__(self, option_strings, dest, **kwargs):
1✔
167
        super().__init__(option_strings, dest, **kwargs)
1✔
168

169
    def __call__(self, parser, namespace, values, option_string=None):
1✔
170
        main_dict = {'param': {}, 'initial': {}}
1✔
171
        for var in values:
1✔
172
            for var_name, (type, val) in var.items():
1✔
173
                main_dict[type][var_name] = val
1✔
174
        setattr(namespace, self.dest, main_dict)
1✔
175

176

177
###########
178
# options #
179
###########
180

181
parser.add_argument(
1✔
182
    '-v', '--version',
183
    action='version', version=f'PySD {__version__}')
184

185
parser.add_argument(
1✔
186
    '-o', '--output-file', dest='output_file',
187
    type=check_output, metavar='FILE',
188
    help='output file to save run outputs (.tab, .csv or .nc)')
189

190
parser.add_argument(
1✔
191
    '-p', '--progress', dest='progress',
192
    action='store_true', default=False,
193
    help='show progress bar during model integration')
194

195
parser.add_argument(
1✔
196
    '-r', '--return-columns', dest='return_columns',
197
    action='store', type=split_columns,
198
    metavar='\'var1, var2, .., varN\' or FILE (.txt)',
199
    help='provide the return columns separated by commas or a .txt file'
200
         ' where each row is a variable')
201

202
parser.add_argument(
1✔
203
    '-e', '--export', dest='export_file',
204
    type=str, metavar='FILE',
205
    help='export to a pickle stateful objects states at the end of the '
206
         'simulation')
207

208
parser.add_argument(
1✔
209
    '-i', '--import-initial', dest='import_file',
210
    type=str, metavar='FILE',
211
    help='import stateful objects states from a pickle file,'
212
         'if given initial conditions from var:value will be ignored')
213

214

215
###################
216
# Model arguments #
217
###################
218

219
model_arguments = parser.add_argument_group(
1✔
220
    'model arguments',
221
    'Modify model control variables.')
222

223
model_arguments.add_argument(
1✔
224
    '-I', '--initial-time', dest='initial_time',
225
    action='store', type=float, metavar='VALUE',
226
    help='modify initial time of the simulation')
227

228
model_arguments.add_argument(
1✔
229
    '-F', '--final-time', dest='final_time',
230
    action='store', type=float, metavar='VALUE',
231
    help='modify final time of the simulation')
232

233
model_arguments.add_argument(
1✔
234
    '-T', '--time-step', dest='time_step',
235
    action='store', type=float, metavar='VALUE',
236
    help='modify time step of the simulation')
237

238
model_arguments.add_argument(
1✔
239
    '-S', '--saveper', dest='saveper',
240
    action='store', type=float, metavar='VALUE',
241
    help='modify time step of the output')
242

243
model_arguments.add_argument(
1✔
244
    '-R', '--return-timestamps', dest='return_timestamps',
245
    action='store', type=split_timestamps,
246
    metavar='\'value1, value2, .., valueN\'',
247
    help='provide the return time stamps separated by commas, if given '
248
         '--saveper will be ignored')
249

250
model_arguments.add_argument(
1✔
251
    '-D', '--data', dest='data_files',
252
    action='store', type=split_files, metavar='\'FILE1, FILE2, .., FILEN\'',
253
    help='input data file or files to run the model')
254

255
#########################
256
# Translation arguments #
257
#########################
258

259
trans_arguments = parser.add_argument_group(
1✔
260
    'translation arguments',
261
    'Configure the translation of the original model.')
262

263
trans_arguments.add_argument(
1✔
264
    '--translate', dest='run',
265
    action='store_false', default=True,
266
    help='only translate the model_file, '
267
         'it does not run it after translation')
268

269
trans_arguments.add_argument(
1✔
270
    '--split-views', dest='split_views',
271
    action='store_true', default=False,
272
    help='parse the sketch to detect model elements in each model view,'
273
         ' and then translate each view in a separate Python file')
274

275
trans_arguments.add_argument(
1✔
276
    '--subview-sep', dest='subview_sep',
277
    action='store', nargs="*", default=[],
278
    metavar='separator_1 separator_2 ... separator_n',
279
    help='further division of views into subviews, by identifying the '
280
         'separator string in the view name, only availabe if --split-views'
281
         ' is used. Passing positional arguments after this argument will'
282
         ' not work')
283

284

285
#######################
286
# Warnings and errors #
287
#######################
288

289
warn_err_arguments = parser.add_argument_group(
1✔
290
    'warning and errors arguments',
291
    'Modify warning and errors management.')
292

293
warn_err_arguments.add_argument(
1✔
294
    '--missing-values', dest='missing_values', default="warning",
295
    action='store', type=str, choices=['warning', 'raise', 'ignore', 'keep'],
296
    help='exception with missing values, \'warning\' (default) shows a '
297
         'warning message and interpolates the values, \'raise\' raises '
298
         'an error, \'ignore\' interpolates the values without showing '
299
         'anything, \'keep\' keeps the missing values')
300

301

302
########################
303
# Positional arguments #
304
########################
305

306
parser.add_argument('model_file', metavar='model_file', type=check_model,
1✔
307
                    help='Vensim, Xmile or PySD model file')
308

309
parser.add_argument('new_values',
1✔
310
                    metavar='variable=new_value', type=split_vars,
311
                    nargs='*', action=SplitVarsAction,
312
                    help='redefine the value of variable with new value. '
313
                    'variable must be a model component, new_value may be a '
314
                    'float or a list of two lists')
315

316
# The destination new_values2 will never be used as the previous argument
317
# is given also with nargs='*'. Nevertheless, the following variable
318
# is declared for documentation
319
parser.add_argument('new_values2',
1✔
320
                    metavar='variable:initial_value', type=split_vars,
321
                    nargs='*', action=SplitVarsAction,
322
                    help='redefine the initial value of variable. '
323
                    'variable must be a model stateful element, initial_value'
324
                    ' must be a float')
325

326

327
#########
328
# Usage #
329
#########
330

331
parser.usage = parser.format_usage().replace("usage: PySD", "python -m pysd")
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc