Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

127

128

129

130

131

132

133

134

135

136

137

138

139

140

141

142

143

144

145

146

147

148

149

150

151

152

153

154

155

156

157

158

159

160

161

162

163

164

165

166

167

168

169

170

171

172

173

174

175

176

177

178

179

180

181

182

183

184

185

186

187

188

189

190

191

192

193

194

195

196

197

198

199

200

201

202

203

204

205

206

207

208

209

210

211

212

213

214

215

216

217

218

219

220

221

222

223

224

225

226

227

228

229

230

231

232

233

234

235

236

237

238

239

240

241

242

243

244

245

246

247

248

249

250

251

252

253

254

255

256

257

258

259

260

261

262

263

264

265

266

267

268

269

270

271

272

273

274

275

276

277

278

279

280

281

282

283

284

285

286

287

288

289

290

291

292

293

294

295

296

297

298

299

300

301

302

303

304

305

306

307

308

309

310

311

312

313

314

315

316

317

318

319

320

321

322

323

324

325

326

327

328

329

330

331

332

333

334

335

336

337

338

339

340

341

342

343

344

345

346

347

348

349

350

351

352

353

354

355

356

357

358

359

360

361

362

363

364

365

366

367

368

369

370

371

372

373

374

375

376

377

378

379

380

381

382

383

384

385

386

387

388

389

390

391

392

393

394

395

396

397

398

399

400

401

402

403

404

405

406

407

408

409

410

411

412

413

414

415

416

417

418

419

420

421

422

423

424

425

426

427

428

429

430

431

432

433

434

435

436

437

438

439

440

441

442

443

444

445

446

447

448

449

450

451

452

453

454

455

456

457

458

459

460

461

462

463

464

465

466

467

468

469

470

471

472

473

474

475

476

477

478

479

480

481

482

483

484

485

486

# system modules 

import textwrap 

import warnings 

import functools 

import types 

import itertools 

import inspect 

import re 

from abc import abstractproperty 

 

# internal modules 

from class_tools.utils import * 

 

# external modules 

 

 

class NotSet: 

""" 

Class to indicate that an argument has not been specified. 

""" 

 

pass 

 

 

def conflicting_arguments(*conflicts): 

""" 

Create a decorator which raises a :any:`ValueError` when the decorated 

function was called with conflicting arguments. Works for both positional 

and keyword arguments. 

 

Args: 

conflicts (sequence of str): the arguments which should not be 

specified together. 

 

Returns: 

callable: a decorator for callables 

""" 

 

def decorator(decorated_fun): 

@functools.wraps(decorated_fun) 

def wrapper(*args, **kwargs): 

spec = inspect.getfullargspec(decorated_fun) 

posargs = dict( 

zip(itertools.chain(spec.args, spec.kwonlyargs), args) 

) 

arguments = set(kwargs).union(set(posargs)) 

if arguments and all(map(arguments.__contains__, conflicts)): 

raise ValueError( 

"function {fun} called with " 

"conflicting arguments: {args}".format( 

fun=repr(decorated_fun.__name__), 

args=", ".join(map(repr, conflicts)), 

) 

) 

 

return decorated_fun(*args, **kwargs) 

 

return wrapper 

 

return decorator 

 

 

def classdecorator(decorated_fun): 

""" 

Decorator for other decorator functions that are supposed to only be 

applied to classes. 

 

Raises: 

ValueError : if the resulting decorator is applied to non-classes 

""" 

 

@functools.wraps(decorated_fun) 

def wrapper(decorated_cls): 

if not isinstance(decorated_cls, type): 

raise TypeError( 

( 

"{cls} object is not a type and " 

"cannot be decorated with this decorator" 

).format(cls=repr(type(decorated_cls).__name__)) 

) 

return decorated_fun(decorated_cls) 

 

return wrapper 

 

 

def add_property(name, *args, abstract=False, **kwargs): 

""" 

Create a :any:`classdecorator` that adds a :any:`property` or 

:any:`abc.abstractproperty` to the decorated class. 

 

Args: 

name (str): the name for the property 

abstract (bool, optional): whether to create an :any:`abstractproperty` 

instead of a :any:`property` 

args, kwargs: arguments passed to :any:`property` (or 

:any:`abstractproperty` of `abstract=True`) 

 

Returns: 

:any:`classdecorator` : decorator for classes 

""" 

 

@classdecorator 

def decorator(decorated_cls): 

prop = (abstractproperty if abstract else property)(*args, **kwargs) 

setattr(decorated_cls, name, prop) 

return decorated_cls 

 

return decorator 

 

 

def readonly_property(name, getter, *args, **kwargs): 

""" 

Create a :any:`classdecorator` that adds a read-only :any:`property` (i.e. 

without setter and deleter). 

 

Args: 

name (str): the name for the constant 

getter (callable): the :any:`property.getter` to use 

args, kwargs: arguments passed to :any:`add_property` 

""" 

return functools.partial(add_property, name, fget=getter)(*args, **kwargs) 

 

 

def constant(name, value, *args, **kwargs): 

""" 

Create a :any:`classdecorator` that adds a :any:`readonly_property` 

returning a static value to the decorated class. 

 

Args: 

name (str): the name for the constant 

value (object): the value of the constant 

args, kwargs: arguments passed to :any:`readonly_property` 

""" 

return functools.partial(readonly_property, name, getter=lambda s: value)( 

*args, **kwargs 

) 

 

 

@conflicting_arguments("static_default", "dynamic_default") 

@conflicting_arguments("static_type", "dynamic_type") 

def wrapper_property( 

name, 

*args, 

attr=NotSet, 

static_default=NotSet, 

dynamic_default=NotSet, 

set_default=False, 

static_type=NotSet, 

dynamic_type=NotSet, 

doc_default=None, 

doc_type=None, 

doc_getter=None, 

doc_setter=None, 

doc_property=None, 

**kwargs 

): 

""" 

Create a :any:`classdecorator` that adds a :any:`property` with getter, 

setter and deleter, wrapping an attribute. 

 

Args: 

name (str): the name for the constant 

attr (str, optional): the name for the wrapped attribute. If unset, 

use ``name`` with an underscore (``_``) prepended. 

static_default (object, optional): value to use in the getter if the 

``attr`` is not yet set 

dynamic_default (object, optional): the return value of this function 

(called with the object as argument) is used in the getter if the 

``attr`` is not yet set. 

set_default (bool, optional): whether to set the ``attr`` to the 

``static_default`` or ``dynamic_default`` (if specified) in the 

getter when it was not yet set. Default is ``False``. 

doc_default, doc_type, doc_getter, doc_setter (str, optional): 

documentation strings. 

static_type (callable, optional): function to convert the value in the 

setter. 

dynamic_type (callable, optional): function to convert the value in the 

setter. Differing from ``static_type``, this function is also 

handed the object reference as first argument. 

args, kwargs: arguments passed to :any:`add_property` 

""" 

# determine the attribute 

if attr is NotSet: 

attr = "".join(("_", name)) 

# determine the getter 

doc_getter_default = None 

if static_default is not NotSet: 

if set_default: 

 

def getter(self): 

try: 

return getattr(self, attr) 

except AttributeError: 

setattr(self, attr, static_default) 

return getattr(self, attr) 

 

else: 

 

def getter(self): 

return getattr(self, attr, static_default) 

 

doc_getter_default = "``{}``".format(repr(static_default)) 

 

elif dynamic_default is not NotSet: 

try: 

s = inspect.getfullargspec(dynamic_default) 

assert len(s.args) == 1 and not any( 

getattr(s, a) 

for a in ( 

"varargs", 

"varkw", 

"defaults", 

"kwonlyargs", 

"kwonlydefaults", 

) 

), "needs to take exactly one positional argument" 

except BaseException as e: # pragma: no cover 

raise ValueError( 

"dynamic_default needs to be " 

"usable as a method: {}".format(e) 

) 

if set_default: 

 

def getter(self): 

try: 

return getattr(self, attr) 

except AttributeError: 

setattr(self, attr, dynamic_default(self)) 

return getattr(self, attr) 

 

else: 

 

def getter(self): 

try: 

return getattr(self, attr) 

except AttributeError: 

return dynamic_default(self) 

 

doc_getter_default = "the return value of {}".format( 

"a user-specified function" 

if (isinstance(dynamic_default, types.LambdaType)) 

else "``{}``".format(dynamic_default.__name__) 

) 

 

else: # no default 

 

def getter(self): 

return getattr(self, attr) 

 

doc_getter = doc_getter or ( 

"Return the value of the ``{attr}`` attribute. " 

+ ( 

( 

"If it hasn't been set yet, " 

"it will be set to the default: {default}" 

) 

if (doc_getter_default and set_default) 

else "" 

) 

).format(attr=attr, default=doc_getter_default) 

 

# determine the setter 

doc_setter_type = None 

if static_type is not NotSet: 

 

def setter(self, new): 

setattr(self, attr, static_type(new)) 

 

doc_setter_type = "new values are modified with {}".format( 

"a user-specified function" 

if (isinstance(static_type, types.LambdaType)) 

else "``{}``".format(static_type.__name__) 

) 

 

elif dynamic_type is not NotSet: 

try: 

s = inspect.getfullargspec(dynamic_type) 

assert len(s.args) == 2 and not any( 

getattr(s, a) 

for a in ( 

"varargs", 

"varkw", 

"defaults", 

"kwonlyargs", 

"kwonlydefaults", 

) 

), "needs to take exactly two positional argument" 

except BaseException as e: # pragma: no cover 

raise ValueError( 

"dynamic_type needs to be " "usable as a method: {}".format(e) 

) 

 

def setter(self, new): 

setattr(self, attr, dynamic_type(self, new)) 

 

doc_setter_type = "new values are modified with {}".format( 

"a user-specified function" 

if (isinstance(dynamic_type, types.LambdaType)) 

else "``{}``".format(dynamic_type.__name__) 

) 

 

else: # no type 

 

def setter(self, new): 

setattr(self, attr, new) 

 

doc_setter = doc_setter or ("Set the ``{attr}`` attribute").format( 

attr=attr 

) 

doc_type = doc_type or doc_setter_type 

 

# create the docstring 

docstring = "\n\n".join( 

filter( 

bool, 

( 

"{doc_property}", 

":type: {doc_type}" if doc_type else "", 

":getter: {doc_getter}" if doc_getter else "", 

":setter: {doc_setter}" if doc_setter else "", 

), 

) 

).format( 

doc_property=doc_property or ("{} property").format(name), 

doc_type=doc_type or doc_setter_type or ":any:`object`", 

doc_getter=doc_getter or "return the property's value", 

doc_setter=doc_setter or "set the property's value", 

) 

 

return functools.partial( 

add_property, 

name, 

fget=getter, 

fset=setter, 

fdel=lambda s: (delattr(s, attr) if hasattr(s, attr) else None), 

doc=docstring, 

)(*args, **kwargs) 

 

 

def with_init_from_properties(): 

""" 

Create a :any:`classdecorator` that **overwrites** the ``__init__``-method 

so that it accepts arguments according to its read- and settable 

properties. 

 

Returns: 

callable : :any:`classdecorator` 

""" 

 

@classdecorator 

def decorator(decorated_cls): 

def __init__(self, **properties): 

cls = type(self) 

for name, prop in get_properties( 

cls, getter=True, setter=True 

).items(): 

if name in properties: 

setattr(self, name, properties.pop(name)) 

for arg, val in properties.items(): 

warnings.warn( 

( 

"{cls}.__init__() got an " 

"unexpected keyword argument {arg}" 

).format(cls=cls.__name__, arg=repr(arg)) 

) 

 

setattr(decorated_cls, "__init__", __init__) 

 

return decorated_cls 

 

return decorator 

 

 

def with_repr_like_init_from_properties(indent=" " * 4, full_path=False): 

""" 

Create a :any:`classdecorator` that **overwrites** the ``__repr__``-method 

so that it returns a representation according to the decorated class' 

properties. 

 

.. note:: 

 

The created ``__repr__``-method assumes that the decorated class' 

``__init__``-method accepts keyword arguments similar to its 

properties. The :any:`with_init_from_properties` :any:`classdecorator` 

creates such an initializer. 

 

Args: 

indent(str, optional): the indentation string. The default is four 

spaces. 

full_path (bool, optional): whether to use the :any:`full_object_path` 

instead of just the object names. Defaults to ``False``. 

 

Returns: 

callable : :any:`classdecorator` 

""" 

 

@classdecorator 

def decorator(decorated_cls): 

def __repr__(self): 

cls = type(self) 

clspath = ( 

full_object_path(type(self)) 

if full_path 

else type(self).__name__ 

) 

 

properties = get_properties(cls, getter=True, setter=True) 

 

# create "prop = {prop}" string tuple for reprformat 

props_kv = tuple( 

map( 

functools.partial(textwrap.indent, prefix=indent), 

map( 

lambda pv: "{p}={v}".format( 

p=pv[0], 

v=re.sub( 

"\n", indent + "\n", repr(pv[1].fget(self)) 

), 

), 

sorted(properties.items()), 

), 

) 

) 

 

# create the format string 

reprformatstr = "{____cls}({args})".format( 

____cls=clspath, 

args=("\n{}\n" if props_kv else "{}").format( 

",\n".join(props_kv) 

), 

) 

 

return reprformatstr.format( 

**{ 

name: repr(prop.fget(self)) 

for name, prop in properties.items() 

} 

) 

 

setattr(decorated_cls, "__repr__", __repr__) 

 

return decorated_cls 

 

return decorator 

 

 

def with_eq_comparing_properties(): 

""" 

Create a :any:`classdecorator` that **overwrites** the ``__eq__``-method so 

that it compares all properties with a getter for equality. 

 

Returns: 

callable : :any:`classdecorator` 

""" 

 

@classdecorator 

def decorator(decorated_cls): 

def __eq__(self, other): 

other_properties = get_properties(other, getter=True) 

for name, prop in get_properties(self, getter=True).items(): 

if name not in other_properties: 

raise TypeError( 

( 

"{other_cls} object has no property " 

"{property} and thus cannot be compared to " 

"{our_cls} object" 

).format( 

other_cls=repr(type(other).__name__), 

property=repr(name), 

our_cls=repr(type(self).__name__), 

) 

) 

if prop.fget(self) != other_properties.get(name).fget(other): 

return False 

return True 

 

__eq__.__doc__ = ( 

"Checks whether all properties of " 

"this object match the corresponding " 

"properties of the given object to compare" 

) 

setattr(decorated_cls, "__eq__", __eq__) 

 

return decorated_cls 

 

return decorator