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

domdfcoding / domdf_python_tools / 3732685666

pending completion
3732685666

push

github

Dominic Davis-Foster
Bump version v3.5.0 -> v3.5.1

2143 of 2200 relevant lines covered (97.41%)

0.97 hits per line

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

94.55
/domdf_python_tools/bases.py
1
#  !/usr/bin/env python
2
#
3
#  bases.py
4
"""
1✔
5
Useful base classes.
6
"""
7
#
8
#  Copyright © 2020 Dominic Davis-Foster <dominic@davis-foster.co.uk>
9
#
10
#  Permission is hereby granted, free of charge, to any person obtaining a copy
11
#  of this software and associated documentation files (the "Software"), to deal
12
#  in the Software without restriction, including without limitation the rights
13
#  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
#  copies of the Software, and to permit persons to whom the Software is
15
#  furnished to do so, subject to the following conditions:
16
#
17
#  The above copyright notice and this permission notice shall be included in all
18
#  copies or substantial portions of the Software.
19
#
20
#  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
21
#  EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
22
#  MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
23
#  IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
24
#  DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
25
#  OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
26
#  OR OTHER DEALINGS IN THE SOFTWARE.
27
#
28
#  UserList based on CPython.
29
#  Licensed under the Python Software Foundation License Version 2.
30
#  Copyright © 2001-2020 Python Software Foundation. All rights reserved.
31
#  Copyright © 2000 BeOpen.com. All rights reserved.
32
#  Copyright © 1995-2000 Corporation for National Research Initiatives. All rights reserved.
33
#  Copyright © 1991-1995 Stichting Mathematisch Centrum. All rights reserved.
34
#
35

36
# stdlib
37
from abc import abstractmethod
1✔
38
from numbers import Real
1✔
39
from pprint import pformat
1✔
40
from typing import (
1✔
41
                Any,
42
                Dict,
43
                Iterable,
44
                Iterator,
45
                List,
46
                MutableSequence,
47
                Optional,
48
                SupportsFloat,
49
                Tuple,
50
                Type,
51
                TypeVar,
52
                Union,
53
                overload
54
                )
55

56
# this package
57
from domdf_python_tools._is_match import is_match_with
1✔
58
from domdf_python_tools.doctools import prettify_docstrings
1✔
59
from domdf_python_tools.typing import SupportsIndex
1✔
60

61
__all__ = [
1✔
62
                "Dictable",
63
                "NamedList",
64
                "namedlist",
65
                "UserList",
66
                "UserFloat",
67
                "Lineup",
68
                "_V",
69
                "_LU",
70
                "_T",
71
                "_S",
72
                "_F",
73
                ]
74

75
_F = TypeVar("_F", bound="UserFloat")
1✔
76
_LU = TypeVar("_LU", bound="Lineup")
1✔
77
_S = TypeVar("_S", bound="UserList")
1✔
78
_T = TypeVar("_T")
1✔
79
_V = TypeVar("_V")
1✔
80

81

82
@prettify_docstrings
1✔
83
class Dictable(Iterable[Tuple[str, _V]]):
1✔
84
        """
85
        The basic structure of a class that can be converted into a dictionary.
86
        """
87

88
        @abstractmethod
1✔
89
        def __init__(self, *args, **kwargs):
1✔
90
                pass
1✔
91

92
        def __repr__(self) -> str:
1✔
93
                return super().__repr__()
1✔
94

95
        def __str__(self) -> str:
1✔
96
                return self.__repr__()
1✔
97

98
        def __iter__(self) -> Iterator[Tuple[str, _V]]:
1✔
99
                """
100
                Iterate over the attributes of the class.
101
                """
102

103
                yield from self.__dict__.items()
1✔
104

105
        def __getstate__(self) -> Dict[str, _V]:
1✔
106
                return self.__dict__
1✔
107

108
        def __setstate__(self, state):
1✔
109
                self.__init__(**state)  # type: ignore
1✔
110

111
        def __copy__(self):
1✔
112
                return self.__class__(**self.__dict__)
1✔
113

114
        def __deepcopy__(self, memodict={}):
1✔
115
                return self.__copy__()
1✔
116

117
        @property
1✔
118
        @abstractmethod
1✔
119
        def __dict__(self):
1✔
120
                return dict()  # pragma: no cover (abc)
121

122
        def __eq__(self, other) -> bool:
1✔
123
                if isinstance(other, self.__class__):
1✔
124
                        return is_match_with(other.__dict__, self.__dict__)
1✔
125

126
                return NotImplemented
1✔
127

128

129
@prettify_docstrings
1✔
130
class UserList(MutableSequence[_T]):
1✔
131
        """
132
        Typed version of :class:`collections.UserList`.
133

134
        Class that simulates a list. The instance’s contents are kept in a regular list,
135
        which is accessible via the :attr:`~.UserList.data` attribute of :class:`~.UserList` instances.
136
        The instance’s contents are initially set to a copy of list, defaulting to the empty list ``[]``.
137

138
        .. versionadded:: 0.10.0
139

140
        :param initlist: The initial values to populate the :class:`~.UserList` with.
141
        :default initlist: ``[]``
142

143
        .. latex:clearpage::
144

145
        .. admonition:: Subclassing requirements
146

147
                Subclasses of :class:`~.UserList` are expected to offer a constructor which can be called with
148
                either no arguments or one argument. List operations which return a new sequence
149
                attempt to create an instance of the actual implementation class. To do so,
150
                it assumes that the constructor can be called with a single parameter, which is a
151
                sequence object used as a data source.
152

153
                If a derived class does not wish to comply with this requirement, all of the special
154
                methods supported by this class will need to be overridden; please consult the
155
                sources for information about the methods which need to be provided in that case.
156
        """
157

158
        #: A real list object used to store the contents of the :class:`~domdf_python_tools.bases.UserList`.
159
        data: List[_T]
1✔
160

161
        def __init__(self, initlist: Optional[Iterable[_T]] = None):
1✔
162
                self.data = []
1✔
163
                if initlist is not None:
1✔
164
                        # XXX should this accept an arbitrary sequence?
165
                        if type(initlist) is type(self.data):  # noqa: E721
1✔
166
                                self.data[:] = initlist
1✔
167
                        elif isinstance(initlist, UserList):
1✔
168
                                self.data[:] = initlist.data[:]
1✔
169
                        else:
170
                                self.data = list(initlist)
1✔
171

172
        def __repr__(self) -> str:
1✔
173
                return repr(self.data)
1✔
174

175
        def __lt__(self, other: object) -> bool:
1✔
176
                return self.data < self.__cast(other)
×
177

178
        def __le__(self, other: object) -> bool:
1✔
179
                return self.data <= self.__cast(other)
×
180

181
        def __eq__(self, other: object) -> bool:
1✔
182
                return self.data == self.__cast(other)
1✔
183

184
        def __gt__(self, other: object) -> bool:
1✔
185
                return self.data > self.__cast(other)
×
186

187
        def __ge__(self, other: object) -> bool:
1✔
188
                return self.data >= self.__cast(other)
×
189

190
        @staticmethod
1✔
191
        def __cast(other):
1✔
192
                return other.data if isinstance(other, UserList) else other
1✔
193

194
        def __contains__(self, item: object) -> bool:
1✔
195
                return item in self.data
1✔
196

197
        def __len__(self) -> int:
1✔
198
                return len(self.data)
1✔
199

200
        def __iter__(self) -> Iterator[_T]:
1✔
201
                yield from self.data
1✔
202

203
        @overload
1✔
204
        def __getitem__(self, i: int) -> _T: ...
1✔
205

206
        @overload
1✔
207
        def __getitem__(self, i: slice) -> MutableSequence[_T]: ...
1✔
208

209
        def __getitem__(self, i: Union[int, slice]) -> Union[_T, MutableSequence[_T]]:
1✔
210
                if isinstance(i, slice):
1✔
211
                        return self.__class__(self.data[i])
1✔
212
                else:
213
                        return self.data[i]
1✔
214

215
        @overload
1✔
216
        def __setitem__(self, i: int, o: _T) -> None: ...
1✔
217

218
        @overload
1✔
219
        def __setitem__(self, i: slice, o: Iterable[_T]) -> None: ...
1✔
220

221
        def __setitem__(self, i: Union[int, slice], item: Union[_T, Iterable[_T]]) -> None:
1✔
222
                self.data[i] = item  # type: ignore
1✔
223

224
        def __delitem__(self, i: Union[int, slice]):
1✔
225
                del self.data[i]
1✔
226

227
        def __add__(self: _S, other: Iterable[_T]) -> _S:
1✔
228
                if isinstance(other, UserList):
1✔
229
                        return self.__class__(self.data + other.data)
1✔
230
                elif isinstance(other, type(self.data)):
1✔
231
                        return self.__class__(self.data + other)
1✔
232
                return self.__class__(self.data + list(other))
1✔
233

234
        def __radd__(self, other):
1✔
235
                if isinstance(other, UserList):
1✔
236
                        return self.__class__(other.data + self.data)
1✔
237
                elif isinstance(other, type(self.data)):
1✔
238
                        return self.__class__(other + self.data)
1✔
239
                return self.__class__(list(other) + self.data)
1✔
240

241
        def __iadd__(self: _S, other: Iterable[_T]) -> _S:
1✔
242
                if isinstance(other, UserList):
1✔
243
                        self.data += other.data
1✔
244
                elif isinstance(other, type(self.data)):
1✔
245
                        self.data += other
1✔
246
                else:
247
                        self.data += list(other)
1✔
248
                return self
1✔
249

250
        def __mul__(self: _S, n: int) -> _S:
1✔
251
                return self.__class__(self.data * n)
1✔
252

253
        __rmul__ = __mul__
1✔
254

255
        def __imul__(self: _S, n: int) -> _S:
1✔
256
                self.data *= n
1✔
257
                return self
1✔
258

259
        def __copy__(self):
1✔
260
                inst = self.__class__.__new__(self.__class__)
×
261
                inst.__dict__.update(self.__dict__)
×
262
                # Create a copy and avoid triggering descriptors
263
                inst.__dict__["data"] = self.__dict__["data"][:]
×
264
                return inst
×
265

266
        def append(self, item: _T) -> None:
1✔
267
                """
268
                Append ``item`` to the end of the :class:`~.domdf_python_tools.bases.UserList`.
269
                """
270

271
                self.data.append(item)
1✔
272

273
        def insert(self, i: int, item: _T) -> None:
1✔
274
                """
275
                Insert ``item`` at position ``i`` in the :class:`~.domdf_python_tools.bases.UserList`.
276
                """
277

278
                self.data.insert(i, item)
1✔
279

280
        def pop(self, i: int = -1) -> _T:
1✔
281
                """
282
                Removes and returns the item at index ``i``.
283

284
                :raises IndexError: if list is empty or index is out of range.
285
                """
286

287
                return self.data.pop(i)
1✔
288

289
        def remove(self, item: _T) -> None:
1✔
290
                """
291
                Removes the first occurrence of ``item`` from the list.
292

293
                :param item:
294

295
                :rtype:
296

297
                :raises ValueError: if the item is not present.
298

299
                .. latex:clearpage::
300
                """
301

302
                self.data.remove(item)
1✔
303

304
        def clear(self) -> None:
1✔
305
                """
306
                Remove all items from the :class:`~.domdf_python_tools.bases.UserList`.
307
                """
308

309
                self.data.clear()
1✔
310

311
        def copy(self: _S) -> _S:
1✔
312
                """
313
                Returns a copy of the :class:`~.domdf_python_tools.bases.UserList`.
314
                """
315

316
                return self.__class__(self)
1✔
317

318
        def count(self, item: _T) -> int:
1✔
319
                """
320
                Returns the number of occurrences of ``item`` in the :class:`~.domdf_python_tools.bases.UserList`.
321
                """
322

323
                return self.data.count(item)
1✔
324

325
        def index(self, item: _T, *args: Any) -> int:
1✔
326
                """
327
                Returns the index of the fist element matching ``item``.
328

329
                :param item:
330
                :param args:
331

332
                :raises ValueError: if the item is not present.
333
                """
334

335
                return self.data.index(item, *args)
1✔
336

337
        def reverse(self) -> None:
1✔
338
                """
339
                Reverse the list in place.
340
                """
341

342
                self.data.reverse()
1✔
343

344
        def sort(self, *, key=None, reverse: bool = False) -> None:
1✔
345
                """
346
                Sort the list in ascending order and return :py:obj:`None`.
347

348
                The sort is in-place (i.e. the list itself is modified) and stable (i.e. the
349
                order of two equal elements is maintained).
350

351
                If a key function is given, apply it once to each list item and sort them,
352
                ascending or descending, according to their function values.
353

354
                The reverse flag can be set to sort in descending order.
355
                """
356

357
                self.data.sort(key=key, reverse=reverse)
1✔
358

359
        def extend(self, other: Iterable[_T]) -> None:
1✔
360
                """
361
                Extend the :class:`~.domdf_python_tools.bases.NamedList` by appending elements from ``other``.
362

363
                :param other:
364
                """
365

366
                if isinstance(other, UserList):
1✔
367
                        self.data.extend(other.data)
1✔
368
                else:
369
                        self.data.extend(other)
1✔
370

371

372
@prettify_docstrings
1✔
373
class UserFloat(Real):
1✔
374
        """
375
        Class which simulates a float.
376

377
        .. versionadded:: 1.6.0
378

379
        :param value: The values to initialise the :class:`~domdf_python_tools.bases.UserFloat` with.
380
        """
381

382
        def __init__(self, value: Union[SupportsFloat, SupportsIndex, str, bytes, bytearray] = 0.0):
1✔
383
                self._value = (float(value), )
1✔
384

385
        def as_integer_ratio(self) -> Tuple[int, int]:
1✔
386
                """
387
                Returns the float as a fraction.
388
                """
389

390
                return float(self).as_integer_ratio()
1✔
391

392
        def hex(self) -> str:  # noqa: A003  # pylint: disable=redefined-builtin
1✔
393
                """
394
                Returns the hexadecimal (base 16) representation of the float.
395
                """
396

397
                return float(self).hex()
1✔
398

399
        def is_integer(self) -> bool:
1✔
400
                """
401
                Returns whether the float is an integer.
402
                """
403

404
                return float(self).is_integer()
1✔
405

406
        @classmethod
1✔
407
        def fromhex(cls: Type[_F], string: str) -> _F:
1✔
408
                """
409
                Create a floating-point number from a hexadecimal string.
410

411
                :param string:
412
                """
413

414
                return cls(float.fromhex(string))
×
415

416
        def __add__(self: _F, other: float) -> _F:
1✔
417
                return self.__class__(float(self).__add__(other))
1✔
418

419
        def __sub__(self: _F, other: float) -> _F:
1✔
420
                return self.__class__(float(self).__sub__(other))
1✔
421

422
        def __mul__(self: _F, other: float) -> _F:
1✔
423
                return self.__class__(float(self).__mul__(other))
1✔
424

425
        def __floordiv__(self: _F, other: float) -> _F:  # type: ignore
1✔
426
                return self.__class__(float(self).__floordiv__(other))
1✔
427

428
        def __truediv__(self: _F, other: float) -> _F:
1✔
429
                return self.__class__(float(self).__truediv__(other))
1✔
430

431
        def __mod__(self: _F, other: float) -> _F:
1✔
432
                return self.__class__(float(self).__mod__(other))
1✔
433

434
        def __divmod__(self: _F, other: float) -> Tuple[_F, _F]:
1✔
435
                return tuple(self.__class__(x) for x in float(self).__divmod__(other))  # type: ignore
×
436

437
        def __pow__(self: _F, other: float, mod=None) -> _F:
1✔
438
                return self.__class__(float(self).__pow__(other, mod))
1✔
439

440
        def __radd__(self: _F, other: float) -> _F:
1✔
441
                return self.__class__(float(self).__radd__(other))
1✔
442

443
        def __rsub__(self: _F, other: float) -> _F:
1✔
444
                return self.__class__(float(self).__rsub__(other))
1✔
445

446
        def __rmul__(self: _F, other: float) -> _F:
1✔
447
                return self.__class__(float(self).__rmul__(other))
1✔
448

449
        def __rfloordiv__(self: _F, other: float) -> _F:  # type: ignore
1✔
450
                return self.__class__(float(self).__rfloordiv__(other))
1✔
451

452
        def __rtruediv__(self: _F, other: float) -> _F:
1✔
453
                return self.__class__(float(self).__rtruediv__(other))
1✔
454

455
        def __rmod__(self: _F, other: float) -> _F:
1✔
456
                return self.__class__(float(self).__rmod__(other))
1✔
457

458
        def __rdivmod__(self: _F, other: float) -> Tuple[_F, _F]:
1✔
459
                return tuple(self.__class__(x) for x in float(self).__rdivmod__(other))  # type: ignore
×
460

461
        def __rpow__(self: _F, other: float, mod=None) -> _F:
1✔
462
                return self.__class__(float(self).__rpow__(other, mod))
1✔
463

464
        def __getnewargs__(self) -> Tuple[float]:
1✔
465
                return self._value
×
466

467
        def __trunc__(self) -> int:
1✔
468
                """
469
                Truncates the float to an integer.
470
                """
471

472
                return float(self).__trunc__()
×
473

474
        def __round__(self, ndigits: Optional[int] = None) -> Union[int, float]:  # type: ignore
1✔
475
                """
476
                Round the :class:`~.UserFloat` to ``ndigits`` decimal places, defaulting to ``0``.
477

478
                If ``ndigits`` is omitted or :py:obj:`None`, returns an :class:`int`,
479
                otherwise returns a :class:`float`.
480
                Rounds half toward even.
481

482
                :param ndigits:
483
                """
484

485
                return float(self).__round__(ndigits)
1✔
486

487
        def __eq__(self, other: object) -> bool:
1✔
488
                if isinstance(other, UserFloat) and not isinstance(other, float):
1✔
489
                        return self._value == other._value
1✔
490
                else:
491
                        return float(self).__eq__(other)
1✔
492

493
        def __ne__(self, other: object) -> bool:
1✔
494
                if isinstance(other, UserFloat) and not isinstance(other, float):
1✔
495
                        return self._value != other._value
1✔
496
                else:
497
                        return float(self).__ne__(other)
1✔
498

499
        def __lt__(self, other: Union[float, "UserFloat"]) -> bool:
1✔
500
                if isinstance(other, UserFloat) and not isinstance(other, float):
1✔
501
                        return self._value < other._value
1✔
502
                else:
503
                        return float(self).__lt__(other)
1✔
504

505
        def __le__(self, other: Union[float, "UserFloat"]) -> bool:
1✔
506
                if isinstance(other, UserFloat) and not isinstance(other, float):
1✔
507
                        return self._value <= other._value
1✔
508
                else:
509
                        return float(self).__le__(other)
1✔
510

511
        def __gt__(self, other: Union[float, "UserFloat"]) -> bool:
1✔
512
                if isinstance(other, UserFloat) and not isinstance(other, float):
1✔
513
                        return self._value > other._value
1✔
514
                else:
515
                        return float(self).__gt__(other)
1✔
516

517
        def __ge__(self, other: Union[float, "UserFloat"]) -> bool:
1✔
518
                if isinstance(other, UserFloat) and not isinstance(other, float):
1✔
519
                        return self._value >= other._value
1✔
520
                else:
521
                        return float(self).__ge__(other)
1✔
522

523
        def __neg__(self: _F) -> _F:
1✔
524
                return self.__class__(float(self).__neg__())
1✔
525

526
        def __pos__(self: _F) -> _F:
1✔
527
                return self.__class__(float(self).__pos__())
1✔
528

529
        def __str__(self) -> str:
1✔
530
                return str(float(self))
1✔
531

532
        def __int__(self) -> int:
1✔
533
                return int(float(self))
1✔
534

535
        def __float__(self) -> float:
1✔
536
                return self._value[0]
1✔
537

538
        def __abs__(self: _F) -> _F:
1✔
539
                return self.__class__(float(self).__abs__())
1✔
540

541
        def __hash__(self) -> int:
1✔
542
                return float(self).__hash__()
1✔
543

544
        def __repr__(self) -> str:
1✔
545
                return str(self)
1✔
546

547
        def __ceil__(self):
1✔
548
                raise NotImplementedError
549

550
        def __floor__(self):
1✔
551
                raise NotImplementedError
552

553
        def __bool__(self) -> bool:
1✔
554
                """
555
                Return ``self != 0``.
556
                """
557

558
                return super().__bool__()
×
559

560
        def __complex__(self) -> complex:
1✔
561
                """
562
                Return :class:`complex(self) <complex>`.
563

564
                .. code-block:: python
565

566
                        complex(self) == complex(float(self), 0)
567
                """
568

569
                return super().__complex__()
×
570

571

572
@prettify_docstrings
1✔
573
class NamedList(UserList[_T]):
1✔
574
        """
575
        A list with a name.
576

577
        The name of the list is taken from the name of the subclass.
578

579
        .. versionchanged:: 0.10.0
580

581
                :class:`~.NamedList` now subclasses :class:`.UserList` rather than :class:`collections.UserList`.
582
        """
583

584
        def __repr__(self) -> str:
1✔
585
                return f"{super().__repr__()}"
1✔
586

587
        def __str__(self) -> str:
1✔
588
                return f"{self.__class__.__name__}{pformat(list(self))}"
1✔
589

590

591
def namedlist(name: str = "NamedList") -> Type[NamedList]:
1✔
592
        """
593
        A factory function to return a custom list subclass with a name.
594

595
        :param name: The name of the list.
596
        """
597

598
        class cls(NamedList):
1✔
599
                pass
1✔
600

601
        cls.__name__ = name
1✔
602

603
        return cls
1✔
604

605

606
class Lineup(UserList[_T]):
1✔
607
        """
608
        List-like type with fluent methods and some star players.
609

610
        .. latex:vspace:: -10px
611
        """
612

613
        def replace(self: _LU, what: _T, with_: _T) -> _LU:
1✔
614
                r"""
615
                Replace the first instance of ``what`` with ``with_``.
616

617
                :param what: The object to find and replace.
618
                :param with\_: The new value for the position in the list.
619
                """
620

621
                self[self.index(what)] = with_
1✔
622

623
                return self
1✔
624

625
        def sort(  # type: ignore
1✔
626
                        self: _LU,
627
                        *,
628
                        key=None,
629
                        reverse: bool = False,
630
                        ) -> _LU:
631
                """
632
                Sort the list in ascending order and return the self.
633

634
                The sort is in-place (i.e. the list itself is modified) and stable (i.e. the
635
                order of two equal elements is maintained).
636

637
                If a key function is given, apply it once to each list item and sort them,
638
                ascending or descending, according to their function values.
639

640
                The reverse flag can be set to sort in descending order.
641
                """
642

643
                super().sort(key=key, reverse=reverse)
1✔
644
                return self
1✔
645

646
        def reverse(self: _LU) -> _LU:  # type: ignore  # noqa: D102
1✔
647
                super().reverse()
1✔
648
                return self
1✔
649

650
        def append(  # type: ignore  # noqa: D102
1✔
651
                        self: _LU,
652
                        item: _T,
653
                        ) -> _LU:
654
                super().append(item)
1✔
655
                return self
1✔
656

657
        def extend(  # type: ignore  # noqa: D102
1✔
658
                        self: _LU,
659
                        other: Iterable[_T],
660
                        ) -> _LU:
661
                super().extend(other)
1✔
662
                return self
1✔
663

664
        def insert(  # type: ignore  # noqa: D102
1✔
665
                        self: _LU,
666
                        i: int,
667
                        item: _T,
668
                        ) -> _LU:
669
                super().insert(i, item)
1✔
670
                return self
1✔
671

672
        def remove(  # type: ignore  # noqa: D102
1✔
673
                        self: _LU,
674
                        item: _T,
675
                        ) -> _LU:
676
                super().remove(item)
1✔
677
                return self
1✔
678

679
        def clear(self: _LU) -> _LU:  # type: ignore  # noqa: D102
1✔
680
                super().clear()
1✔
681
                return self
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