genlayer-test 0.4.0__py3-none-any.whl → 0.5.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. {genlayer_test-0.4.0.dist-info → genlayer_test-0.5.0.dist-info}/METADATA +257 -24
  2. genlayer_test-0.5.0.dist-info/RECORD +76 -0
  3. gltest/__init__.py +7 -6
  4. gltest/{glchain/client.py → clients.py} +1 -1
  5. gltest/contracts/__init__.py +4 -0
  6. gltest/contracts/contract.py +205 -0
  7. gltest/{glchain/contract.py → contracts/contract_factory.py} +47 -144
  8. gltest/contracts/contract_functions.py +62 -0
  9. gltest/contracts/method_stats.py +163 -0
  10. gltest/contracts/stats_collector.py +259 -0
  11. gltest/contracts/utils.py +12 -0
  12. gltest/fixtures.py +2 -6
  13. gltest/helpers/take_snapshot.py +1 -1
  14. gltest/logging.py +17 -0
  15. gltest/types.py +1 -0
  16. gltest_cli/config/constants.py +2 -0
  17. gltest_cli/config/plugin.py +121 -49
  18. gltest_cli/config/pytest_context.py +9 -0
  19. gltest_cli/config/types.py +73 -8
  20. gltest_cli/config/user.py +71 -28
  21. gltest_cli/logging.py +4 -3
  22. tests/examples/contracts/football_prediction_market.py +1 -1
  23. tests/examples/tests/test_football_prediction_market.py +2 -2
  24. tests/examples/tests/test_intelligent_oracle_factory.py +8 -24
  25. tests/examples/tests/test_llm_erc20.py +5 -5
  26. tests/examples/tests/test_llm_erc20_analyze.py +50 -0
  27. tests/examples/tests/test_log_indexer.py +23 -11
  28. tests/examples/tests/test_multi_file_contract.py +2 -2
  29. tests/examples/tests/test_multi_file_contract_legacy.py +2 -2
  30. tests/examples/tests/test_multi_read_erc20.py +14 -12
  31. tests/examples/tests/test_multi_tenant_storage.py +11 -7
  32. tests/examples/tests/test_read_erc20.py +1 -1
  33. tests/examples/tests/test_storage.py +4 -4
  34. tests/examples/tests/test_storage_legacy.py +5 -3
  35. tests/examples/tests/test_user_storage.py +20 -10
  36. tests/examples/tests/test_wizard_of_coin.py +1 -1
  37. tests/gltest_cli/config/test_config_integration.py +432 -0
  38. tests/gltest_cli/config/test_general_config.py +406 -0
  39. tests/gltest_cli/config/test_plugin.py +164 -1
  40. tests/gltest_cli/config/test_user.py +61 -1
  41. genlayer_test-0.4.0.dist-info/RECORD +0 -66
  42. gltest/glchain/__init__.py +0 -16
  43. {genlayer_test-0.4.0.dist-info → genlayer_test-0.5.0.dist-info}/WHEEL +0 -0
  44. {genlayer_test-0.4.0.dist-info → genlayer_test-0.5.0.dist-info}/entry_points.txt +0 -0
  45. {genlayer_test-0.4.0.dist-info → genlayer_test-0.5.0.dist-info}/licenses/LICENSE +0 -0
  46. {genlayer_test-0.4.0.dist-info → genlayer_test-0.5.0.dist-info}/top_level.txt +0 -0
  47. /gltest/{glchain/account.py → accounts.py} +0 -0
@@ -0,0 +1,259 @@
1
+ """
2
+ Stats collector module for contract method analysis.
3
+
4
+ This module contains classes and functions to collect and analyze statistics
5
+ from contract method executions, simplifying the analyze_method implementation.
6
+ """
7
+
8
+ import time
9
+ import json
10
+ import hashlib
11
+ from datetime import datetime, timezone
12
+ from typing import List, Dict, Any, Optional
13
+ from dataclasses import dataclass
14
+
15
+ from gltest.clients import get_gl_client
16
+ from gltest.types import CalldataEncodable
17
+ from .method_stats import StateGroup, FailedRun, MethodStatsDetailed, MethodStatsSummary
18
+ from gltest_cli.config.general import get_general_config
19
+ from gltest_cli.config.pytest_context import get_current_test_nodeid
20
+ from .utils import safe_filename
21
+
22
+
23
+ @dataclass
24
+ class SimulationConfig:
25
+ """Configuration for simulation runs."""
26
+
27
+ provider: str
28
+ model: str
29
+ config: Optional[Dict[str, Any]] = None
30
+ plugin: Optional[str] = None
31
+ plugin_config: Optional[Dict[str, Any]] = None
32
+
33
+
34
+ @dataclass
35
+ class SimulationResults:
36
+ """Results from simulation runs."""
37
+
38
+ sim_results: List[Dict[str, Any]]
39
+ failed_runs: List[FailedRun]
40
+ server_errors: int
41
+ execution_time: float
42
+ timestamp: str
43
+
44
+
45
+ class StatsCollector:
46
+ """Collects and analyzes statistics for contract method executions."""
47
+
48
+ def __init__(
49
+ self,
50
+ contract_address: str,
51
+ method_name: str,
52
+ account: Any,
53
+ args: Optional[List[CalldataEncodable]] = None,
54
+ ):
55
+ self.contract_address = contract_address
56
+ self.method_name = method_name
57
+ self.account = account
58
+ self.args = args or []
59
+ self.client = get_gl_client()
60
+
61
+ def run_simulations(
62
+ self, sim_config: SimulationConfig, runs: int
63
+ ) -> SimulationResults:
64
+ """Execute multiple simulation runs and collect results."""
65
+ start_time = time.time()
66
+ timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
67
+
68
+ sim_results = []
69
+ failed_runs_list = []
70
+ server_errors = 0
71
+
72
+ for run_idx in range(runs):
73
+ try:
74
+ sim_result = self._execute_single_simulation(sim_config)
75
+ sim_results.append(sim_result)
76
+
77
+ if sim_result.get("execution_result") != "SUCCESS":
78
+ failed_runs_list.append(
79
+ self._create_failed_run(run_idx, sim_result)
80
+ )
81
+ except Exception as e:
82
+ server_errors += 1
83
+ failed_runs_list.append(
84
+ FailedRun(
85
+ run=run_idx,
86
+ error=str(e),
87
+ error_type="server",
88
+ genvm_result=None,
89
+ )
90
+ )
91
+
92
+ execution_time = time.time() - start_time
93
+
94
+ return SimulationResults(
95
+ sim_results=sim_results,
96
+ failed_runs=failed_runs_list,
97
+ server_errors=server_errors,
98
+ execution_time=execution_time,
99
+ timestamp=timestamp,
100
+ )
101
+
102
+ def _execute_single_simulation(
103
+ self, sim_config: SimulationConfig
104
+ ) -> Dict[str, Any]:
105
+ """Execute a single simulation."""
106
+ config_dict = {
107
+ "provider": sim_config.provider,
108
+ "model": sim_config.model,
109
+ }
110
+
111
+ if (
112
+ sim_config.config is not None
113
+ and sim_config.plugin is not None
114
+ and sim_config.plugin_config is not None
115
+ ):
116
+ config_dict["config"] = sim_config.config
117
+ config_dict["plugin"] = sim_config.plugin
118
+ config_dict["plugin_config"] = sim_config.plugin_config
119
+
120
+ return self.client.simulate_write_contract(
121
+ address=self.contract_address,
122
+ function_name=self.method_name,
123
+ account=self.account,
124
+ args=self.args,
125
+ sim_config=config_dict,
126
+ )
127
+
128
+ def _create_failed_run(self, run_idx: int, sim_result: Dict[str, Any]) -> FailedRun:
129
+ """Create a FailedRun object from a failed simulation result."""
130
+ return FailedRun(
131
+ run=run_idx,
132
+ error=sim_result.get("error", "unknown error"),
133
+ error_type="simulation",
134
+ genvm_result={
135
+ "stderr": sim_result.get("genvm_result", {}).get("stderr", ""),
136
+ "stdout": sim_result.get("genvm_result", {}).get("stdout", ""),
137
+ },
138
+ )
139
+
140
+ def analyze_results(
141
+ self,
142
+ sim_results: SimulationResults,
143
+ runs: int,
144
+ sim_config: SimulationConfig,
145
+ ) -> MethodStatsSummary:
146
+ """Analyze simulation results and generate statistics."""
147
+ state_groups = self._analyze_states(sim_results.sim_results)
148
+
149
+ executed_runs = runs - sim_results.server_errors
150
+ successful_runs = sum(
151
+ 1
152
+ for sim_receipt in sim_results.sim_results
153
+ if sim_receipt.get("execution_result") == "SUCCESS"
154
+ )
155
+
156
+ most_common_count = max((group.count for group in state_groups), default=0)
157
+ reliability_score = (
158
+ (most_common_count / executed_runs) if executed_runs > 0 else 0.0
159
+ )
160
+
161
+ # Save detailed stats
162
+ detailed_stats = self._create_detailed_stats(
163
+ sim_results=sim_results,
164
+ state_groups=state_groups,
165
+ runs=runs,
166
+ executed_runs=executed_runs,
167
+ successful_runs=successful_runs,
168
+ most_common_count=most_common_count,
169
+ reliability_score=reliability_score,
170
+ sim_config=sim_config,
171
+ )
172
+ self._save_detailed_stats(detailed_stats)
173
+
174
+ # Return summary
175
+ return MethodStatsSummary(
176
+ method=self.method_name,
177
+ args=self.args,
178
+ total_runs=runs,
179
+ server_error_runs=sim_results.server_errors,
180
+ executed_runs=executed_runs,
181
+ failed_runs=len(sim_results.failed_runs),
182
+ successful_runs=successful_runs,
183
+ unique_states=len(state_groups),
184
+ most_common_state_count=most_common_count,
185
+ reliability_score=reliability_score,
186
+ execution_time=sim_results.execution_time,
187
+ provider=sim_config.provider,
188
+ model=sim_config.model,
189
+ )
190
+
191
+ def _analyze_states(self, sim_results: List[Dict[str, Any]]) -> List[StateGroup]:
192
+ """Analyze contract states from simulation results."""
193
+ state_counts = {}
194
+ state_to_hash_str = {}
195
+
196
+ for sim_receipt in sim_results:
197
+ contract_state = sim_receipt.get("contract_state", {})
198
+ state_json = json.dumps(contract_state, sort_keys=True)
199
+ state_hash = hashlib.sha256(state_json.encode()).hexdigest()
200
+ state_hash_str = f"0x{state_hash}"
201
+ state_to_hash_str[state_hash] = state_hash_str
202
+ state_counts[state_hash] = state_counts.get(state_hash, 0) + 1
203
+
204
+ return [
205
+ StateGroup(count=count, state_hash=state_to_hash_str[state_hash])
206
+ for state_hash, count in sorted(
207
+ state_counts.items(), key=lambda x: x[1], reverse=True
208
+ )
209
+ ]
210
+
211
+ def _create_detailed_stats(
212
+ self,
213
+ sim_results: SimulationResults,
214
+ state_groups: List[StateGroup],
215
+ runs: int,
216
+ executed_runs: int,
217
+ successful_runs: int,
218
+ most_common_count: int,
219
+ reliability_score: float,
220
+ sim_config: SimulationConfig,
221
+ ) -> MethodStatsDetailed:
222
+ """Create detailed statistics object."""
223
+ configuration = {
224
+ "runs": runs,
225
+ "provider": sim_config.provider,
226
+ "model": sim_config.model,
227
+ "config": sim_config.config,
228
+ "plugin": sim_config.plugin,
229
+ "plugin_config": sim_config.plugin_config,
230
+ }
231
+
232
+ return MethodStatsDetailed(
233
+ method=self.method_name,
234
+ params=self.args,
235
+ timestamp=sim_results.timestamp,
236
+ configuration=configuration,
237
+ execution_time=sim_results.execution_time,
238
+ executed_runs=executed_runs,
239
+ failed_runs=len(sim_results.failed_runs),
240
+ successful_runs=successful_runs,
241
+ server_error_runs=sim_results.server_errors,
242
+ unique_states=len(state_groups),
243
+ most_common_state_count=most_common_count,
244
+ reliability_score=reliability_score,
245
+ state_groups=state_groups,
246
+ failed_runs_results=sim_results.failed_runs,
247
+ sim_results=sim_results.sim_results,
248
+ )
249
+
250
+ def _save_detailed_stats(self, detailed_stats: MethodStatsDetailed) -> None:
251
+ """Save detailed statistics to the configured directory."""
252
+ general_config = get_general_config()
253
+ current_nodeid = get_current_test_nodeid()
254
+ if current_nodeid is None:
255
+ safe_name = "no_test"
256
+ else:
257
+ safe_name = safe_filename(current_nodeid)
258
+ stats_dir = general_config.get_analysis_dir() / safe_name
259
+ detailed_stats.save_to_directory(stats_dir)
@@ -0,0 +1,12 @@
1
+ def safe_filename(filename: str) -> str:
2
+ """
3
+ Replace problematic characters in filename.
4
+ """
5
+ return (
6
+ filename.replace("/", "_")
7
+ .replace("\\", "_")
8
+ .replace(":", "_")
9
+ .replace("-", "_")
10
+ .replace(" ", "_")
11
+ .replace(".py", "")
12
+ )
gltest/fixtures.py CHANGED
@@ -4,12 +4,8 @@ These fixtures can be imported and used in test files.
4
4
  """
5
5
 
6
6
  import pytest
7
- from gltest.glchain import (
8
- get_gl_client,
9
- get_accounts,
10
- get_default_account,
11
- get_gl_provider,
12
- )
7
+ from gltest.clients import get_gl_client, get_gl_provider
8
+ from gltest.accounts import get_accounts, get_default_account
13
9
  from gltest_cli.config.general import get_general_config
14
10
 
15
11
 
@@ -1,4 +1,4 @@
1
- from gltest.glchain import get_gl_provider
1
+ from gltest.clients import get_gl_provider
2
2
  from dataclasses import dataclass
3
3
  from typing import Callable
4
4
  from gltest.exceptions import HelperError, InvalidSnapshotError
gltest/logging.py ADDED
@@ -0,0 +1,17 @@
1
+ import logging
2
+
3
+
4
+ def setup_logger() -> logging.Logger:
5
+ """Setup a logger for the gltest package - disabled by default"""
6
+
7
+ logger = logging.getLogger("gltest")
8
+
9
+ logger.setLevel(logging.NOTSET)
10
+ logger.disabled = True
11
+ logger.addHandler(logging.NullHandler())
12
+ return logger
13
+
14
+
15
+ logger = setup_logger()
16
+
17
+ __all__ = ["logger"]
gltest/types.py CHANGED
@@ -4,4 +4,5 @@ from genlayer_py.types import (
4
4
  GenLayerTransaction,
5
5
  TransactionStatus,
6
6
  CalldataEncodable,
7
+ TransactionHashVariant,
7
8
  )
@@ -4,7 +4,9 @@ from pathlib import Path
4
4
 
5
5
  GLTEST_CONFIG_FILE = "gltest.config.yaml"
6
6
  DEFAULT_NETWORK = "localnet"
7
+ PRECONFIGURED_NETWORKS = ["localnet", "studionet", "testnet_asimov"]
7
8
  DEFAULT_RPC_URL = SIMULATOR_JSON_RPC_URL
8
9
  DEFAULT_ENVIRONMENT = ".env"
9
10
  DEFAULT_CONTRACTS_DIR = Path("contracts")
11
+ DEFAULT_ARTIFACTS_DIR = Path("artifacts")
10
12
  DEFAULT_NETWORK_ID = 61999
@@ -1,4 +1,6 @@
1
+ import pytest
1
2
  from pathlib import Path
3
+ import shutil
2
4
  from gltest_cli.logging import logger
3
5
  from gltest_cli.config.user import (
4
6
  user_config_exists,
@@ -9,6 +11,7 @@ from gltest_cli.config.general import (
9
11
  get_general_config,
10
12
  )
11
13
  from gltest_cli.config.types import PluginConfig
14
+ from gltest_cli.config.pytest_context import _pytest_context
12
15
 
13
16
 
14
17
  def pytest_addoption(parser):
@@ -20,17 +23,24 @@ def pytest_addoption(parser):
20
23
  help="Path to directory containing contract files",
21
24
  )
22
25
 
26
+ group.addoption(
27
+ "--artifacts-dir",
28
+ action="store",
29
+ default=None,
30
+ help="Path to directory for storing contract artifacts",
31
+ )
32
+
23
33
  group.addoption(
24
34
  "--default-wait-interval",
25
35
  action="store",
26
- default=10000,
36
+ default=3000,
27
37
  help="Default interval (ms) between transaction receipt checks",
28
38
  )
29
39
 
30
40
  group.addoption(
31
41
  "--default-wait-retries",
32
42
  action="store",
33
- default=15,
43
+ default=50,
34
44
  help="Default number of retries for transaction receipt checks",
35
45
  )
36
46
 
@@ -55,61 +65,123 @@ def pytest_addoption(parser):
55
65
  help="Test with mocks",
56
66
  )
57
67
 
68
+ group.addoption(
69
+ "--leader-only",
70
+ action="store_true",
71
+ default=False,
72
+ help="Run contracts in leader-only mode",
73
+ )
58
74
 
59
- def pytest_configure(config):
60
- general_config = get_general_config()
61
75
 
62
- # Handle user config from gltest.config.yaml
63
- if not user_config_exists():
64
- logger.warning(
65
- "File `gltest.config.yaml` not found in the current directory, using default config"
76
+ def pytest_configure(config):
77
+ try:
78
+ general_config = get_general_config()
79
+
80
+ network_name = config.getoption("--network")
81
+
82
+ if not user_config_exists():
83
+ logger.warning(
84
+ "File `gltest.config.yaml` not found in the current directory, using default config, create a `gltest.config.yaml` file to manage multiple networks"
85
+ )
86
+ user_config = get_default_user_config()
87
+
88
+ # Special handling for testnet_asimov - check if accounts are configured
89
+ if network_name == "testnet_asimov":
90
+ logger.error(
91
+ "For testnet_asimov, you need to configure accounts in gltest.config.yaml, see https://docs.genlayer.com/api-references/genlayer-test"
92
+ )
93
+ pytest.exit("gltest configuration error")
94
+ else:
95
+ logger.info(
96
+ "File `gltest.config.yaml` found in the current directory, using it"
97
+ )
98
+ user_config = load_user_config("gltest.config.yaml")
99
+
100
+ general_config.user_config = user_config
101
+
102
+ # Handle plugin config from command line
103
+ contracts_dir = config.getoption("--contracts-dir")
104
+ artifacts_dir = config.getoption("--artifacts-dir")
105
+ default_wait_interval = config.getoption("--default-wait-interval")
106
+ default_wait_retries = config.getoption("--default-wait-retries")
107
+ rpc_url = config.getoption("--rpc-url")
108
+ network = config.getoption("--network")
109
+ test_with_mocks = config.getoption("--test-with-mocks")
110
+ leader_only = config.getoption("--leader-only")
111
+
112
+ plugin_config = PluginConfig()
113
+ plugin_config.contracts_dir = (
114
+ Path(contracts_dir) if contracts_dir is not None else None
66
115
  )
67
- logger.info("Create a `gltest.config.yaml` file to manage multiple networks")
68
- user_config = get_default_user_config()
69
- else:
70
- logger.info(
71
- "File `gltest.config.yaml` found in the current directory, using it"
116
+ plugin_config.artifacts_dir = (
117
+ Path(artifacts_dir) if artifacts_dir is not None else None
72
118
  )
73
- user_config = load_user_config("gltest.config.yaml")
74
-
75
- general_config.user_config = user_config
76
-
77
- # Handle plugin config from command line
78
- contracts_dir = config.getoption("--contracts-dir")
79
- default_wait_interval = config.getoption("--default-wait-interval")
80
- default_wait_retries = config.getoption("--default-wait-retries")
81
- rpc_url = config.getoption("--rpc-url")
82
- network = config.getoption("--network")
83
- test_with_mocks = config.getoption("--test-with-mocks")
119
+ plugin_config.default_wait_interval = int(default_wait_interval)
120
+ plugin_config.default_wait_retries = int(default_wait_retries)
121
+ plugin_config.rpc_url = rpc_url
122
+ plugin_config.network_name = network
123
+ plugin_config.test_with_mocks = test_with_mocks
124
+ plugin_config.leader_only = leader_only
84
125
 
85
- plugin_config = PluginConfig()
86
- plugin_config.contracts_dir = (
87
- Path(contracts_dir) if contracts_dir is not None else None
88
- )
89
- plugin_config.default_wait_interval = int(default_wait_interval)
90
- plugin_config.default_wait_retries = int(default_wait_retries)
91
- plugin_config.rpc_url = rpc_url
92
- plugin_config.network_name = network
93
- plugin_config.test_with_mocks = test_with_mocks
94
-
95
- general_config.plugin_config = plugin_config
126
+ general_config.plugin_config = plugin_config
127
+ except Exception as e:
128
+ logger.error(f"Gltest configure error: {e}")
129
+ pytest.exit("gltest configuration error")
96
130
 
97
131
 
98
132
  def pytest_sessionstart(session):
99
- general_config = get_general_config()
100
- logger.info("Using the following configuration:")
101
- logger.info(f" RPC URL: {general_config.get_rpc_url()}")
102
- logger.info(f" Selected Network: {general_config.get_network_name()}")
103
- logger.info(
104
- f" Available networks: {list(general_config.user_config.networks.keys())}"
105
- )
106
- logger.info(f" Contracts directory: {general_config.get_contracts_dir()}")
107
- logger.info(f" Environment: {general_config.user_config.environment}")
108
- logger.info(
109
- f" Default wait interval: {general_config.get_default_wait_interval()} ms"
110
- )
111
- logger.info(f" Default wait retries: {general_config.get_default_wait_retries()}")
112
- logger.info(f" Test with mocks: {general_config.get_test_with_mocks()}")
133
+ try:
134
+ general_config = get_general_config()
135
+ artifacts_dir = general_config.get_artifacts_dir()
136
+ if artifacts_dir and artifacts_dir.exists():
137
+ logger.info(f"Clearing artifacts directory: {artifacts_dir}")
138
+ try:
139
+ shutil.rmtree(artifacts_dir)
140
+ artifacts_dir.mkdir(parents=True, exist_ok=True)
141
+ except Exception as e:
142
+ logger.warning(f"Failed to clear artifacts directory: {e}")
143
+ elif artifacts_dir:
144
+ artifacts_dir.mkdir(parents=True, exist_ok=True)
145
+ logger.info("Using the following configuration:")
146
+ logger.info(f" RPC URL: {general_config.get_rpc_url()}")
147
+ logger.info(f" Selected Network: {general_config.get_network_name()}")
148
+ # Show available networks including preconfigured ones
149
+ all_networks = general_config.get_networks_keys()
150
+ logger.info(f" Available networks: {all_networks}")
151
+ logger.info(f" Contracts directory: {general_config.get_contracts_dir()}")
152
+ logger.info(f" Artifacts directory: {general_config.get_artifacts_dir()}")
153
+ logger.info(f" Environment: {general_config.user_config.environment}")
154
+ logger.info(
155
+ f" Default wait interval: {general_config.get_default_wait_interval()} ms"
156
+ )
157
+ logger.info(
158
+ f" Default wait retries: {general_config.get_default_wait_retries()}"
159
+ )
160
+ logger.info(f" Test with mocks: {general_config.get_test_with_mocks()}")
161
+
162
+ if (
163
+ general_config.get_leader_only()
164
+ and not general_config.check_studio_based_rpc()
165
+ ):
166
+ logger.warning(
167
+ "Leader only mode: True (enabled on non-studio network - will have no effect)"
168
+ )
169
+ else:
170
+ logger.info(f" Leader only mode: {general_config.get_leader_only()}")
171
+ except Exception as e:
172
+ logger.error(f"Gltest session start error: {e}")
173
+ pytest.exit("gltest session start error")
174
+
175
+
176
+ def pytest_runtest_setup(item):
177
+ _pytest_context.current_item = item
178
+
179
+
180
+ def pytest_runtest_teardown(item):
181
+ try:
182
+ del _pytest_context.current_item
183
+ except AttributeError:
184
+ pass
113
185
 
114
186
 
115
187
  pytest_plugins = ["gltest.fixtures"]
@@ -0,0 +1,9 @@
1
+ from typing import Optional
2
+ import threading
3
+
4
+ _pytest_context = threading.local()
5
+
6
+
7
+ def get_current_test_nodeid() -> Optional[str]:
8
+ item = getattr(_pytest_context, "current_item", None)
9
+ return item.nodeid if item is not None else None