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

SwissDataScienceCenter / renku-python / 5948282216

23 Aug 2023 07:22AM UTC coverage: 43.128% (-42.8%) from 85.902%
5948282216

push

github-actions

Ralf Grubenmann
fix(service): remove doctor check from cache.migrations_check (#3597)

0 of 1 new or added line in 1 file covered. (0.0%)

10566 existing lines in 224 files now uncovered.

10524 of 24402 relevant lines covered (43.13%)

0.43 hits per line

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

70.4
/renku/command/command_builder/command.py
1
# Copyright Swiss Data Science Center (SDSC). A partnership between
2
# École Polytechnique Fédérale de Lausanne (EPFL) and
3
# Eidgenössische Technische Hochschule Zürich (ETHZ).
4
#
5
# Licensed under the Apache License, Version 2.0 (the "License");
6
# you may not use this file except in compliance with the License.
7
# You may obtain a copy of the License at
8
#
9
#     http://www.apache.org/licenses/LICENSE-2.0
10
#
11
# Unless required by applicable law or agreed to in writing, software
12
# distributed under the License is distributed on an "AS IS" BASIS,
13
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
# See the License for the specific language governing permissions and
15
# limitations under the License.
16
"""Command builder."""
1✔
17

18
import contextlib
1✔
19
import functools
1✔
20
import threading
1✔
21
from collections import defaultdict
1✔
22
from pathlib import Path
1✔
23
from typing import Any, Callable, Dict, List, Optional, Type, Union
1✔
24

25
import inject
1✔
26

27
from renku.core import errors
1✔
28
from renku.core.util.communication import CommunicationCallback
1✔
29
from renku.core.util.git import get_git_path
1✔
30
from renku.domain_model.project_context import project_context
1✔
31

32
_LOCAL = threading.local()
1✔
33

34

35
def check_finalized(f):
1✔
36
    """Decorator to prevent modification of finalized builders.
37

38
    Args:
39
        f: Decorated function.
40

41
    Returns:
42
        Wrapped function.
43
    """
44

45
    @functools.wraps(f)
1✔
46
    def wrapper(*args, **kwargs):
1✔
47
        """Decorator to prevent modification of finalized builders."""
48
        if not args or not isinstance(args[0], Command):
1✔
49
            raise errors.ParameterError("Command hooks need to be `Command` object methods.")
×
50

51
        if args[0].finalized:
1✔
52
            raise errors.CommandFinalizedError("Cannot modify a finalized `Command`.")
×
53

54
        return f(*args, **kwargs)
1✔
55

56
    return wrapper
1✔
57

58

59
def _patched_get_injector_or_die() -> inject.Injector:
1✔
60
    """Patched version of get_injector_or_die with thread local injectors.
61

62
    Allows deferred definition of an injector per thread.
63
    """
64
    injector = getattr(_LOCAL, "injector", None)
1✔
65
    if not injector:
1✔
UNCOV
66
        raise inject.InjectorException("No injector is configured")
×
67

68
    return injector
1✔
69

70

71
def _patched_configure(config: Optional[inject.BinderCallable] = None, bind_in_runtime: bool = True) -> inject.Injector:
1✔
72
    """Create an injector with a callable config or raise an exception when already configured.
73

74
    Args:
75
        config(Optional[inject.BinderCallable], optional): Injection binding config (Default value = None).
76
        bind_in_runtime(bool, optional): Whether to allow binding at runtime (Default value = True).
77

78
    Returns:
79
        Injector: Thread-safe injector with bindings applied.
80
    """
81

82
    if getattr(_LOCAL, "injector", None):
1✔
83
        raise inject.InjectorException("Injector is already configured")
×
84

85
    _LOCAL.injector = inject.Injector(config, bind_in_runtime=bind_in_runtime)
1✔
86

87
    return _LOCAL.injector
1✔
88

89

90
inject.configure = _patched_configure
1✔
91
inject.get_injector_or_die = _patched_get_injector_or_die
1✔
92

93

94
def remove_injector():
1✔
95
    """Remove a thread-local injector."""
96
    if getattr(_LOCAL, "injector", None):
1✔
97
        del _LOCAL.injector
1✔
98

99

100
@contextlib.contextmanager
1✔
101
def replace_injection(bindings: Dict, constructor_bindings=None):
1✔
102
    """Temporarily inject various test objects.
103

104
    Args:
105
        bindings: New normal injection bindings to apply.
106
        constructor_bindings: New constructor bindings to apply (Default value = None).
107
    """
UNCOV
108
    constructor_bindings = constructor_bindings or {}
×
109

UNCOV
110
    def bind(binder):
×
UNCOV
111
        for key, value in bindings.items():
×
112
            binder.bind(key, value)
×
UNCOV
113
        for key, value in constructor_bindings.items():
×
UNCOV
114
            binder.bind_to_constructor(key, value)
×
115

UNCOV
116
    old_injector = getattr(_LOCAL, "injector", None)
×
UNCOV
117
    try:
×
UNCOV
118
        if old_injector:
×
119
            remove_injector()
×
UNCOV
120
        inject.configure(bind, bind_in_runtime=False)
×
121

UNCOV
122
        yield
×
123
    finally:
UNCOV
124
        remove_injector()
×
125

UNCOV
126
        if old_injector:
×
127
            _LOCAL.injector = old_injector
×
128

129

130
class Command:
1✔
131
    """Base renku command builder."""
132

133
    HOOK_ORDER = 1
1✔
134

135
    def __init__(self) -> None:
1✔
136
        """__init__ of Command."""
137
        self.injection_pre_hooks: Dict[int, List[Callable]] = defaultdict(list)
1✔
138
        self.pre_hooks: Dict[int, List[Callable]] = defaultdict(list)
1✔
139
        self.post_hooks: Dict[int, List[Callable]] = defaultdict(list)
1✔
140
        self._operation: Optional[Callable] = None
1✔
141
        self._finalized: bool = False
1✔
142
        self._track_std_streams: bool = False
1✔
143
        self._working_directory: Optional[str] = None
1✔
144
        self._context_added: bool = False
1✔
145

146
    def __getattr__(self, name: str) -> Any:
1✔
147
        """Bubble up attributes of wrapped builders."""
148
        if "_builder" in self.__dict__:
1✔
UNCOV
149
            return getattr(self._builder, name)
×
150

151
        raise AttributeError(f"{self.__class__.__name__} object has no attribute {name}")
1✔
152

153
    def __setattr__(self, name: str, value: Any) -> None:
1✔
154
        """Set attributes of wrapped builders."""
155
        if hasattr(self, "_builder") and self.__class__ is not self._builder.__class__:
1✔
156
            self._builder.__setattr__(name, value)
1✔
157

158
        object.__setattr__(self, name, value)
1✔
159

160
    def _injection_pre_hook(self, builder: "Command", context: dict, *args, **kwargs) -> None:
1✔
161
        """Setup dependency injections.
162

163
        Args:
164
            builder("Command"): Current ``CommandBuilder``.
165
            context(dict): Current context dictionary.
166
        """
167
        if not project_context.has_context():
1✔
UNCOV
168
            path = get_git_path(self._working_directory or ".")
×
UNCOV
169
            project_context.push_path(path)
×
UNCOV
170
            self._context_added = True
×
171

172
        context["bindings"] = {}
1✔
173
        context["constructor_bindings"] = {}
1✔
174

175
    def _pre_hook(self, builder: "Command", context: dict, *args, **kwargs) -> None:
1✔
176
        """Setup project.
177

178
        Args:
179
            builder("Command"): Current ``CommandBuilder``.
180
            context(dict): Current context dictionary.
181
        """
182

183
        stack = contextlib.ExitStack()
1✔
184
        context["stack"] = stack
1✔
185

186
    def _post_hook(self, builder: "Command", context: dict, result: "CommandResult", *args, **kwargs) -> None:
1✔
187
        """Post-hook method.
188

189
        Args:
190
            builder("Command"): Current ``CommandBuilder``.
191
            context(dict): Current context dictionary.
192
            result("CommandResult"): Result of command execution.
193
        """
194
        remove_injector()
1✔
195

196
        if self._context_added:
1✔
UNCOV
197
            project_context.pop_context()
×
198

199
        if result.error:
1✔
UNCOV
200
            raise result.error
×
201

202
    def execute(self, *args, **kwargs) -> "CommandResult":
1✔
203
        """Execute the wrapped operation.
204

205
        First executes `pre_hooks` in ascending `order`, passing a read/write context between them.
206
        It then calls the wrapped `operation`. The result of the operation then gets pass to all the `post_hooks`,
207
        but in descending `order`. It then returns the result or error if there was one.
208

209
        Returns:
210
            CommandResult: Result of execution of command.
211
        """
212
        if not self.finalized:
1✔
213
            raise errors.CommandNotFinalizedError("Call `build()` before executing a command")
×
214

215
        context: Dict[str, Any] = {}
1✔
216
        if any(self.injection_pre_hooks):
1✔
217
            order = sorted(self.injection_pre_hooks.keys())
1✔
218

219
            for o in order:
1✔
220
                for hook in self.injection_pre_hooks[o]:
1✔
221
                    hook(self, context, *args, **kwargs)
1✔
222

223
        def _bind(binder):
1✔
224
            for key, value in context["bindings"].items():
1✔
225
                binder.bind(key, value)
×
226
            for key, value in context["constructor_bindings"].items():
1✔
227
                binder.bind_to_constructor(key, value)
1✔
228

229
            return binder
1✔
230

231
        inject.configure(_bind, bind_in_runtime=False)
1✔
232

233
        if any(self.pre_hooks):
1✔
234
            order = sorted(self.pre_hooks.keys())
1✔
235

236
            for o in order:
1✔
237
                for hook in self.pre_hooks[o]:
1✔
238
                    try:
1✔
239
                        hook(self, context, *args, **kwargs)
1✔
UNCOV
240
                    except (Exception, BaseException):
×
241
                        # don't leak injections from failed hook
UNCOV
242
                        remove_injector()
×
UNCOV
243
                        raise
×
244

245
        output = None
1✔
246
        error = None
1✔
247

248
        try:
1✔
249
            with context["stack"]:
1✔
250
                output = self._operation(*args, **kwargs)  # type: ignore
1✔
UNCOV
251
        except errors.RenkuException as e:
×
UNCOV
252
            error = e
×
UNCOV
253
        except (Exception, BaseException):
×
UNCOV
254
            remove_injector()
×
UNCOV
255
            raise
×
256

257
        result = CommandResult(output, error, CommandResult.FAILURE if error else CommandResult.SUCCESS)
1✔
258

259
        if any(self.post_hooks):
1✔
260
            order = sorted(self.post_hooks.keys(), reverse=True)
1✔
261

262
            for o in order:
1✔
263
                for hook in self.post_hooks[o]:
1✔
264
                    hook(self, context, result, *args, **kwargs)
1✔
265

266
        return result
1✔
267

268
    @property
1✔
269
    def finalized(self) -> bool:
1✔
270
        """Whether this builder is still being constructed or has been finalized."""
271
        if hasattr(self, "_builder"):
1✔
272
            return self._builder.finalized
1✔
273
        return self._finalized
1✔
274

275
    def any_builder_is_instance_of(self, cls: Type) -> bool:
1✔
276
        """Check if any 'chained' command builder is an instance of a specific command builder class."""
277
        if isinstance(self, cls):
×
278
            return True
×
279
        elif "_builder" in self.__dict__:
×
280
            return self._builder.any_builder_is_instance_of(cls)
×
281
        else:
282
            return False
×
283

284
    @property
1✔
285
    def will_write_to_database(self) -> bool:
1✔
286
        """Will running the command write anything to the metadata store."""
287
        try:
×
288
            return self._write
×
289
        except AttributeError:
×
290
            return False
×
291

292
    @check_finalized
1✔
293
    def add_injection_pre_hook(self, order: int, hook: Callable):
1✔
294
        """Add a pre-execution hook for dependency injection.
295

296
        Args:
297
            order(int): Determines the order of executed hooks, lower numbers get executed first.
298
            hook(Callable): The hook to add.
299
        """
300
        if hasattr(self, "_builder"):
1✔
UNCOV
301
            self._builder.add_injection_pre_hook(order, hook)
×
302
        else:
303
            self.injection_pre_hooks[order].append(hook)
1✔
304

305
    @check_finalized
1✔
306
    def add_pre_hook(self, order: int, hook: Callable):
1✔
307
        """Add a pre-execution hook.
308

309
        Args:
310
            order(int): Determines the order of executed hooks, lower numbers get executed first.
311
            hook(Callable): The hook to add.
312
        """
313
        if hasattr(self, "_builder"):
1✔
UNCOV
314
            self._builder.add_pre_hook(order, hook)
×
315
        else:
316
            self.pre_hooks[order].append(hook)
1✔
317

318
    @check_finalized
1✔
319
    def add_post_hook(self, order: int, hook: Callable):
1✔
320
        """Add a post-execution hook.
321

322
        Args:
323
            order(int): Determines the order of executed hooks, higher numbers get executed first.
324
            hook(Callable): The hook to add.
325
        """
326
        if hasattr(self, "_builder"):
1✔
UNCOV
327
            self._builder.add_post_hook(order, hook)
×
328
        else:
329
            self.post_hooks[order].append(hook)
1✔
330

331
    @check_finalized
1✔
332
    def build(self) -> "Command":
1✔
333
        """Build (finalize) the command.
334

335
        Returns:
336
            Command: Finalized command that cannot be modified.
337
        """
338
        if not self._operation:
1✔
339
            raise errors.ConfigurationError("`Command` needs to have a wrapped `command` set")
×
340
        self.add_injection_pre_hook(self.HOOK_ORDER, self._injection_pre_hook)
1✔
341
        self.add_pre_hook(self.HOOK_ORDER, self._pre_hook)
1✔
342
        self.add_post_hook(self.HOOK_ORDER, self._post_hook)
1✔
343

344
        self._finalized = True
1✔
345

346
        return self
1✔
347

348
    @check_finalized
1✔
349
    def command(self, operation: Callable):
1✔
350
        """Set the wrapped command.
351

352
        Args:
353
            operation(Callable): The function to wrap in the command builder.
354

355
        Returns:
356
            Command: This command.
357
        """
358

359
        self._operation = operation
1✔
360

361
        return self
1✔
362

363
    @check_finalized
1✔
364
    def working_directory(self, directory: str) -> "Command":
1✔
365
        """Set the working directory for the command.
366

367
        WARNING: Should not be used in the core service.
368

369
        Args:
370
            directory(str): The working directory to work in.
371

372
        Returns:
373
            Command: This command.
374
        """
375
        self._working_directory = directory
×
376

377
        return self
×
378

379
    @check_finalized
1✔
380
    def track_std_streams(self) -> "Command":
1✔
381
        """Whether to track STD streams or not.
382

383
        Returns:
384
            Command: This command.
385
        """
386
        self._track_std_streams = True
×
387

388
        return self
×
389

390
    @check_finalized
1✔
391
    def with_git_isolation(self) -> "Command":
1✔
392
        """Whether to run in git isolation or not."""
UNCOV
393
        from renku.command.command_builder.repo import Isolation
×
394

UNCOV
395
        return Isolation(self)
×
396

397
    @check_finalized
1✔
398
    def with_commit(
1✔
399
        self,
400
        message: Optional[str] = None,
401
        commit_if_empty: bool = False,
402
        raise_if_empty: bool = False,
403
        commit_only: Optional[Union[str, List[Union[str, Path]]]] = None,
404
        skip_staging: bool = False,
405
        skip_dirty_checks: bool = False,
406
    ) -> "Command":
407
        """Create a commit.
408

409
        Args:
410
            message(str, optional): The commit message. Auto-generated if left empty (Default value = None).
411
            commit_if_empty(bool, optional): Whether to commit if there are no modified files (Default value = False).
412
            raise_if_empty(bool, optional): Whether to raise an exception if there are no modified files
413
                (Default value = False).
414
            commit_only(bool, optional): Only commit the supplied paths (Default value = None).
415
            skip_staging(bool): Don't commit staged files.
416
            skip_dirty_checks(bool): Don't check if paths are dirty or staged.
417
        """
UNCOV
418
        from renku.command.command_builder.repo import Commit
×
419

UNCOV
420
        return Commit(
×
421
            self,
422
            message=message,
423
            commit_if_empty=commit_if_empty,
424
            raise_if_empty=raise_if_empty,
425
            commit_only=commit_only,
426
            skip_staging=skip_staging,
427
            skip_dirty_checks=skip_dirty_checks,
428
        )
429

430
    @check_finalized
1✔
431
    def lock_project(self) -> "Command":
1✔
432
        """Acquire a lock for the whole project."""
UNCOV
433
        from renku.command.command_builder.lock import ProjectLock
×
434

UNCOV
435
        return ProjectLock(self)
×
436

437
    @check_finalized
1✔
438
    def lock_dataset(self) -> "Command":
1✔
439
        """Acquire a lock for a dataset."""
UNCOV
440
        from renku.command.command_builder.lock import DatasetLock
×
441

UNCOV
442
        return DatasetLock(self)
×
443

444
    @check_finalized
1✔
445
    def require_migration(self) -> "Command":
1✔
446
        """Check if a migration is needed."""
UNCOV
447
        from renku.command.command_builder.migration import RequireMigration
×
448

UNCOV
449
        return RequireMigration(self)
×
450

451
    @check_finalized
1✔
452
    def require_clean(self) -> "Command":
1✔
453
        """Check that the repository is clean."""
UNCOV
454
        from renku.command.command_builder.repo import RequireClean
×
455

UNCOV
456
        return RequireClean(self)
×
457

458
    @check_finalized
1✔
459
    def with_communicator(self, communicator: CommunicationCallback) -> "Command":
1✔
460
        """Create a communicator.
461

462
        Args:
463
            communicator(CommunicationCallback): Communicator to use for writing to user.
464
        """
UNCOV
465
        from renku.command.command_builder.communication import Communicator
×
466

UNCOV
467
        return Communicator(self, communicator)
×
468

469
    @check_finalized
1✔
470
    def with_database(self, write: bool = False, path: Optional[str] = None, create: bool = False) -> "Command":
1✔
471
        """Provide an object database connection.
472

473
        Args:
474
            write(bool, optional): Whether or not to persist changes to the database (Default value = False).
475
            path(str, optional): Location of the database (Default value = None).
476
            create(bool, optional): Whether the database should be created if it doesn't exist (Default value = False).
477
        """
478
        from renku.command.command_builder.database import DatabaseCommand
1✔
479

480
        return DatabaseCommand(self, write, path, create)
1✔
481

482

483
class CommandResult:
1✔
484
    """The result of a command.
485

486
    The return value of the command is set as `.output`, if there was an error, it is set as `.error`, and
487
    the status of the command is set to either `CommandResult.SUCCESS` or CommandResult.FAILURE`.
488
    """
489

490
    SUCCESS = 0
1✔
491

492
    FAILURE = 1
1✔
493

494
    def __init__(self, output, error, status) -> None:
1✔
495
        """__init__ of CommandResult."""
496
        self.output = output
1✔
497
        self.error = error
1✔
498
        self.status = status
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

© 2025 Coveralls, Inc