Skip to content

Commit 5625415

Browse files
authored
Release 5.12.1
2 parents 7b70172 + f63dc75 commit 5625415

File tree

12 files changed

+220
-67
lines changed

12 files changed

+220
-67
lines changed

Base/src/ecflow/base/ClientOptionsParser.cpp

+101-51
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,63 @@ bool is_valid_path(const std::string& path) {
2020
return !path.empty() && path[0] == '/';
2121
}
2222

23-
} // namespace
23+
void parse_option(ClientOptionsParser::option_t& option,
24+
ClientOptionsParser::option_set_t& processed_options,
25+
ClientOptionsParser::arguments_set_t& args) {
26+
if (auto found = args[0].find('='); found == std::string::npos) {
27+
// In case form 1) is used, we discard the '--<option>'
28+
args.erase(args.begin());
29+
// store the 'arg1'
30+
option.value.push_back(args.front());
31+
option.original_tokens.push_back(args.front());
32+
}
33+
else {
34+
// Otherwise, form 2) is used, and the first positional value must be
35+
// taken by tokenizing the option '--<option>=<arg1>'
36+
std::string arg = args[0].substr(found + 1);
37+
option.value.push_back(arg);
38+
option.original_tokens.push_back(arg);
39+
}
40+
// Discard the option at the front
41+
args.erase(args.begin());
42+
}
2443

25-
ClientOptionsParser::option_set_t ClientOptionsParser::operator()(ClientOptionsParser::arguments_set_t& args) {
26-
option_set_t processed_options;
44+
template <typename PREDICATE>
45+
void parse_positional_arguments(
46+
ClientOptionsParser::option_t& option,
47+
ClientOptionsParser::option_set_t& processed_options,
48+
ClientOptionsParser::arguments_set_t& args,
49+
size_t maximum_positional_args,
50+
PREDICATE predicate = [](const std::string&) { return true; }) {
51+
52+
// Collect up to N positional arguments, that satisfy the predicate
53+
for (size_t i = 0; i < maximum_positional_args && !args.empty(); ++i) {
54+
// Take each of the positional values as an option value
55+
std::string& arg = args.front();
56+
// Once we find the first argument that doesn't satify the predicate,
57+
// we stop collecting arguments
58+
if (!predicate(arg)) {
59+
break;
60+
}
61+
option.value.push_back(arg);
62+
option.original_tokens.push_back(arg);
63+
64+
// Remove the argument value
65+
args.erase(args.begin());
66+
}
67+
}
68+
69+
void parse_positional_arguments(ClientOptionsParser::option_t& option,
70+
ClientOptionsParser::option_set_t& processed_options,
71+
ClientOptionsParser::arguments_set_t& args,
72+
size_t maximum_positional_args) {
73+
74+
// Collect up to N positional arguments
75+
parse_positional_arguments(
76+
option, processed_options, args, maximum_positional_args, [](const std::string&) { return true; });
77+
}
78+
79+
void parse_alter(ClientOptionsParser::option_set_t& processed_options, ClientOptionsParser::arguments_set_t& args) {
2780

2881
// *** Important! ***
2982
// This custom handler is needed to ensure that the "--alter" option
@@ -36,59 +89,56 @@ ClientOptionsParser::option_set_t ClientOptionsParser::operator()(ClientOptionsP
3689
// 1) --alter arg1 arg2 arg3 (arg4) path [path [path [...]]]
3790
// 2) --alter=arg1 arg2 arg3 (arg4) path [path [path [...]]]
3891

39-
if (ecf::algorithm::starts_with(args[0], "--alter")) {
92+
ClientOptionsParser::option_t alter{std::string{"alter"}, {}};
4093

41-
option_t alter{std::string{"alter"}, {}};
94+
parse_option(alter, processed_options, args);
4295

43-
if (auto found = args[0].find('='); found == std::string::npos) {
44-
// In case form 1) is used, we discard the '--alter'
45-
args.erase(args.begin());
46-
// store the 'arg1'
47-
alter.value.push_back(args.front());
48-
alter.original_tokens.push_back(args.front());
49-
}
50-
else {
51-
// Otherwise, form 2) is used, and the first positional value must be
52-
// taken by tokenizing the option '--alter=arg1'
53-
std::string arg = args[0].substr(found + 1);
54-
alter.value.push_back(arg);
55-
alter.original_tokens.push_back(arg);
56-
}
57-
// Discard the option at the front
58-
args.erase(args.begin());
96+
// Collect up to 4 positional arguments, that are not paths
97+
parse_positional_arguments(
98+
alter, processed_options, args, 4, [](const std::string& arg) { return !is_valid_path(arg); });
5999

60-
// Collect (non path) positional arguments
61-
const size_t maximum_positional_args = 4;
62-
for (size_t i = 0; i < maximum_positional_args && !args.empty(); ++i) {
63-
// Take each of the positional values as an option value
64-
std::string& arg = args.front();
65-
// Once we find the first path argument we stop collecting arguments
66-
if (is_valid_path(arg)) {
67-
break;
68-
}
69-
alter.value.push_back(arg);
70-
alter.original_tokens.push_back(arg);
71-
72-
// Remove the argument value
73-
args.erase(args.begin());
74-
}
100+
// Collect remaining positional arguments, that are paths
101+
parse_positional_arguments(
102+
alter, processed_options, args, std::numeric_limits<size_t>::max(), [](const std::string& arg) {
103+
return is_valid_path(arg);
104+
});
75105

76-
// Collect only paths
77-
for (; !args.empty();) {
78-
// Take each of the positional values as an option value
79-
std::string& arg = args.front();
80-
// Once we find a (non path) argument, we stop collections arguments
81-
if (!is_valid_path(arg)) {
82-
break;
83-
}
84-
alter.value.push_back(arg);
85-
alter.original_tokens.push_back(arg);
86-
87-
// Remove the argument value
88-
args.erase(args.begin());
89-
}
106+
processed_options.push_back(alter);
107+
}
108+
109+
void parse_label(ClientOptionsParser::option_set_t& processed_options, ClientOptionsParser::arguments_set_t& args) {
110+
111+
// *** Important! ***
112+
// This custom handler is needed to ensure that the "--alter" option
113+
// special value parameters are handled correctly. For example,
114+
// values starting with -, such as "-j64".
115+
//
116+
// The custom handling will consider that 2 positional values (not
117+
// to be confused with positional arguments) are provided with the
118+
// --label option, as per one of the following forms:
119+
// 1) --label arg1 arg2
120+
// 2) --label=arg1 arg2
121+
122+
ClientOptionsParser::option_t label{std::string{"label"}, {}};
123+
124+
parse_option(label, processed_options, args);
90125

91-
processed_options.push_back(alter);
126+
// Collect 1 positional argument (i.e. the label value)
127+
parse_positional_arguments(label, processed_options, args, 1);
128+
129+
processed_options.push_back(label);
130+
}
131+
132+
} // namespace
133+
134+
ClientOptionsParser::option_set_t ClientOptionsParser::operator()(ClientOptionsParser::arguments_set_t& args) {
135+
option_set_t processed_options;
136+
137+
if (ecf::algorithm::starts_with(args[0], "--alter")) {
138+
parse_alter(processed_options, args);
139+
}
140+
else if (ecf::algorithm::starts_with(args[0], "--label")) {
141+
parse_label(processed_options, args);
92142
}
93143
return processed_options;
94144
}

Base/src/ecflow/base/stc/SStringCmd.cpp

+3-1
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,9 @@ bool SStringCmd::handle_server_response(ServerReply& server_reply, Cmd_ptr cts_c
3434
if (debug)
3535
cout << " SStringCmd::handle_server_response str.size()= " << str_.size() << "\n";
3636
if (server_reply.cli())
37-
std::cout << str_ << "\n";
37+
// The following uses std::endl to ensure the output is flushed.
38+
// This is necessary when called from the Python API, otherwise the output cannot be captured systematically.
39+
std::cout << str_ << std::endl;
3840
else
3941
server_reply.set_string(str_);
4042
return true;

CMakeLists.txt

+1-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ endif()
2525
find_package( ecbuild 3.4 REQUIRED HINTS ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/../ecbuild ) # Before project()
2626

2727
# this will generate variables, see ACore/ecflow_version.h.in
28-
project( ecflow LANGUAGES CXX VERSION 5.12.0 )
28+
project( ecflow LANGUAGES CXX VERSION 5.12.1 )
2929

3030
set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${CMAKE_SOURCE_DIR}/cmake)
3131

Client/test/TestClientOptions.cpp

+70
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
#include <boost/test/unit_test.hpp>
1212

13+
#include "ecflow/base/cts/task/LabelCmd.hpp"
1314
#include "ecflow/base/cts/user/AlterCmd.hpp"
1415
#include "ecflow/client/ClientEnvironment.hpp"
1516
#include "ecflow/client/ClientInvoker.hpp"
@@ -21,6 +22,27 @@
2122
/// \brief Tests the capabilities of ClientOptions
2223
///
2324

25+
template <typename REQUIRE>
26+
void test_label(const CommandLine& cl, REQUIRE check) {
27+
std::cout << "Testing command line: " << cl.original() << std::endl;
28+
29+
ClientOptions options;
30+
ClientEnvironment environment(false);
31+
// Setup environment with some defaults
32+
environment.set_child_path("/path/to/child");
33+
environment.set_child_password("password");
34+
try {
35+
auto base_command = options.parse(cl, &environment);
36+
auto derived_command = dynamic_cast<LabelCmd*>(base_command.get());
37+
38+
BOOST_REQUIRE(derived_command);
39+
check(*derived_command, environment);
40+
}
41+
catch (boost::program_options::unknown_option& e) {
42+
BOOST_FAIL(std::string("Unexpected exception caught: ") + e.what());
43+
}
44+
}
45+
2446
template <typename REQUIRE>
2547
void test_alter(const CommandLine& cl, REQUIRE check) {
2648
std::cout << "Testing command line: " << cl.original() << std::endl;
@@ -724,6 +746,54 @@ BOOST_AUTO_TEST_CASE(test_is_able_handle_alter_delete) {
724746
} // paths
725747
}
726748

749+
BOOST_AUTO_TEST_CASE(test_is_able_to_handle_label) {
750+
{
751+
auto cl = CommandLine::make_command_line("ecflow_client",
752+
"--label=name",
753+
"some value with spaces");
754+
test_label(cl, [&](const LabelCmd& command, const ClientEnvironment& env) {
755+
BOOST_REQUIRE_EQUAL(command.name(), "name");
756+
BOOST_REQUIRE_EQUAL(command.label(), "some value with spaces");
757+
});
758+
}
759+
{
760+
auto cl = CommandLine::make_command_line("ecflow_client",
761+
"--label=name",
762+
R"(some "quoted" value)");
763+
test_label(cl, [&](const LabelCmd& command, const ClientEnvironment& env) {
764+
BOOST_REQUIRE_EQUAL(command.name(), "name");
765+
BOOST_REQUIRE_EQUAL(command.label(), R"(some "quoted" value)");
766+
});
767+
}
768+
{
769+
auto cl = CommandLine::make_command_line("ecflow_client",
770+
"--label=name",
771+
"-j64");
772+
test_label(cl, [&](const LabelCmd& command, const ClientEnvironment& env) {
773+
BOOST_REQUIRE_EQUAL(command.name(), "name");
774+
BOOST_REQUIRE_EQUAL(command.label(), "-j64");
775+
});
776+
}
777+
{
778+
auto cl = CommandLine::make_command_line("ecflow_client",
779+
"--label=name",
780+
"--long-option");
781+
test_label(cl, [&](const LabelCmd& command, const ClientEnvironment& env) {
782+
BOOST_REQUIRE_EQUAL(command.name(), "name");
783+
BOOST_REQUIRE_EQUAL(command.label(), "--long-option");
784+
});
785+
}
786+
{
787+
auto cl = CommandLine::make_command_line("ecflow_client",
788+
"--label=name",
789+
"--option=value");
790+
test_label(cl, [&](const LabelCmd& command, const ClientEnvironment& env) {
791+
BOOST_REQUIRE_EQUAL(command.name(), "name");
792+
BOOST_REQUIRE_EQUAL(command.label(), "--option=value");
793+
});
794+
}
795+
}
796+
727797
BOOST_AUTO_TEST_SUITE_END()
728798

729799
BOOST_AUTO_TEST_SUITE_END()

Pyext/ecflow/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,6 @@
1515
The ecFlow python module
1616
"""
1717

18-
__version__ = '5.12.0'
18+
__version__ = '5.12.1'
1919

2020
# http://stackoverflow.com/questions/13040646/how-do-i-create-documentation-with-pydoc

Pyext/src/ecflow/python/ClientDoc.cpp

+4-3
Original file line numberDiff line numberDiff line change
@@ -680,13 +680,14 @@ const char* ClientDoc::ping() {
680680
}
681681

682682
const char* ClientDoc::stats() {
683-
return "Prints the `ecflow_server`_ statistics to standard out\n::\n\n"
684-
" void stats()\n"
683+
return "Returns the `ecflow_server`_ statistics as a string\n::\n\n"
684+
" string stats()\n"
685685
"\nUsage:\n\n"
686686
".. code-block:: python\n\n"
687687
" try:\n"
688688
" ci = Client() # use default host(ECF_HOST) & port(ECF_PORT)\n"
689-
" ci.stats()\n"
689+
" stats = ci.stats()\n"
690+
" print(stats)\n"
690691
" except RuntimeError, e:\n"
691692
" print(str(e))\n";
692693
}

Pyext/src/ecflow/python/ExportClient.cpp

+6-3
Original file line numberDiff line numberDiff line change
@@ -130,9 +130,12 @@ class CliSetter {
130130
private:
131131
ClientInvoker* _self;
132132
};
133-
void stats(ClientInvoker* self) {
134-
CliSetter setter(self);
133+
const std::string& stats(ClientInvoker* self) {
135134
self->stats();
135+
// The statistics are printed to stdout for backward compatibility with previous python client versions
136+
// TODO: remove printing to stdout in the next release
137+
std::cout << self->server_reply().get_string() << std::endl;
138+
return self->server_reply().get_string();
136139
}
137140
void stats_reset(ClientInvoker* self) {
138141
self->stats_reset();
@@ -574,7 +577,7 @@ void export_Client() {
574577
.def("free_all_dep", &free_all_dep, ClientDoc::free_all_dep())
575578
.def("free_all_dep", &free_all_dep1)
576579
.def("ping", &ClientInvoker::pingServer, ClientDoc::ping())
577-
.def("stats", &stats, ClientDoc::stats())
580+
.def("stats", &stats, return_value_policy<copy_const_reference>(), ClientDoc::stats())
578581
.def("stats_reset", &stats_reset, ClientDoc::stats_reset())
579582
.def("get_file",
580583
&get_file,

Pyext/test/py_s_TestClientApi.py

+4-2
Original file line numberDiff line numberDiff line change
@@ -534,12 +534,14 @@ def test_client_free_dep(ci):
534534

535535
def test_client_stats(ci):
536536
print_test(ci,"test_client_stats")
537-
ci.stats() # writes to standard out
537+
stats = ci.stats()
538+
assert "statistics" in s, "Expected statistics in the response"
538539

539540
def test_client_stats_reset(ci):
540541
print_test(ci,"test_client_stats_reset")
541542
ci.stats_reset()
542-
ci.stats() # should produce no output, where we measure requests
543+
statis = ci.stats()
544+
assert "statistics" in s, "Expected statistics in the response"
543545

544546
def test_client_debug_server_on_off(ci):
545547
print_test(ci,"test_client_debug_server_on_off")

docs/conf.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@
9393

9494

9595
def get_ecflow_version():
96-
version = "5.12.0"
96+
version = "5.12.1"
9797
ecflow_version = version.split(".")
9898
print("Extracted ecflow version '" + str(ecflow_version))
9999
return ecflow_version

docs/python_api/Client.rst

+5-4
Original file line numberDiff line numberDiff line change
@@ -2259,21 +2259,22 @@ Usage:
22592259
sort_attributes( (Client)arg1, (list)paths, (str)attribute_name [, (bool)recursive=True]) -> None
22602260
22612261
2262-
.. py:method:: Client.stats( (Client)arg1) -> None :
2262+
.. py:method:: Client.stats( (Client)arg1) -> str :
22632263
:module: ecflow
22642264
2265-
Prints the :term:`ecflow_server` statistics to standard out
2265+
Returns the :term:`ecflow_server` statistics as a string
22662266
::
22672267
2268-
void stats()
2268+
string stats()
22692269
22702270
Usage:
22712271
22722272
.. code-block:: python
22732273
22742274
try:
22752275
ci = Client() # use default host(ECF_HOST) & port(ECF_PORT)
2276-
ci.stats()
2276+
stats = ci.stats()
2277+
print(stats)
22772278
except RuntimeError, e:
22782279
print(str(e))
22792280

docs/release_notes/index.rst

+1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ Release notes
44
.. toctree::
55
:maxdepth: 1
66

7+
version_5.12.1
78
version_5.12
89
version_5.11.4
910
version_5.11.3

0 commit comments

Comments
 (0)