or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

advanced-types.mdbasic-types.mdconfiguration.mdcontainer-types.mdcore-traits.mdindex.mdlinking.mdobservers.md

observers.mddocs/

0

# Observers and Decorators

1

2

Decorators and handlers for observing trait changes, validating values, and providing dynamic defaults. These enable reactive programming patterns and sophisticated validation logic in trait-based classes.

3

4

## Capabilities

5

6

### Observer Decorators

7

8

Decorators for registering functions to respond to trait changes.

9

10

```python { .api }

11

def observe(*names, type='change'):

12

"""

13

Decorator to observe trait changes.

14

15

Registers a method to be called when specified traits change.

16

The decorated method receives a change dictionary with details

17

about the change event.

18

19

Parameters:

20

- *names: str - Trait names to observe (empty for all traits)

21

- type: str - Type of change to observe ('change' is most common)

22

23

Returns:

24

function - Decorator function

25

26

Usage:

27

@observe('trait_name', 'other_trait')

28

def _trait_changed(self, change):

29

# Handle change

30

pass

31

"""

32

33

def observe_compat(*names, type='change'):

34

"""

35

Backward-compatibility decorator for observers.

36

37

Provides compatibility with older observer patterns while

38

using the new observe decorator internally.

39

40

Parameters:

41

- *names: str - Trait names to observe

42

- type: str - Type of change to observe

43

44

Returns:

45

function - Decorator function

46

"""

47

```

48

49

### Validator Decorators

50

51

Decorators for registering cross-validation functions.

52

53

```python { .api }

54

def validate(*names):

55

"""

56

Decorator to register cross-validator.

57

58

Registers a method to validate trait values before they are set.

59

The validator can modify the value or raise TraitError to reject it.

60

61

Parameters:

62

- *names: str - Trait names to validate

63

64

Returns:

65

function - Decorator function

66

67

Usage:

68

@validate('trait_name')

69

def _validate_trait(self, proposal):

70

# proposal is dict with 'value' key

71

value = proposal['value']

72

# Validate and possibly modify value

73

return validated_value

74

"""

75

```

76

77

### Default Value Decorators

78

79

Decorators for providing dynamic default values.

80

81

```python { .api }

82

def default(name):

83

"""

84

Decorator for dynamic default value generator.

85

86

Registers a method to provide default values for traits when

87

they are first accessed and no value has been set.

88

89

Parameters:

90

- name: str - Trait name to provide default for

91

92

Returns:

93

function - Decorator function

94

95

Usage:

96

@default('trait_name')

97

def _default_trait(self):

98

return computed_default_value

99

"""

100

```

101

102

### Event Handler Classes

103

104

Classes representing different types of event handlers.

105

106

```python { .api }

107

class EventHandler:

108

"""

109

Base class for event handlers.

110

111

Provides common functionality for all types of event handlers

112

including observer, validator, and default handlers.

113

"""

114

115

class ObserveHandler(EventHandler):

116

"""

117

Handler for observe decorator.

118

119

Manages observation of trait changes and dispatching

120

to registered handler methods.

121

"""

122

123

class ValidateHandler(EventHandler):

124

"""

125

Handler for validate decorator.

126

127

Manages validation of trait values before assignment

128

and dispatching to registered validator methods.

129

"""

130

131

class DefaultHandler(EventHandler):

132

"""

133

Handler for default decorator.

134

135

Manages generation of default values for traits

136

and dispatching to registered default methods.

137

"""

138

```

139

140

## Usage Examples

141

142

### Basic Change Observation

143

144

```python

145

from traitlets import HasTraits, Unicode, Int, observe

146

147

class Person(HasTraits):

148

name = Unicode()

149

age = Int()

150

151

@observe('name')

152

def _name_changed(self, change):

153

print(f"Name changed from '{change['old']}' to '{change['new']}'")

154

155

@observe('age')

156

def _age_changed(self, change):

157

print(f"Age changed from {change['old']} to {change['new']}")

158

159

@observe('name', 'age')

160

def _any_change(self, change):

161

print(f"Trait '{change['name']}' changed on {change['owner']}")

162

163

person = Person()

164

person.name = "Alice" # Triggers _name_changed and _any_change

165

person.age = 30 # Triggers _age_changed and _any_change

166

person.name = "Bob" # Triggers _name_changed and _any_change

167

```

168

169

### Change Event Details

170

171

```python

172

from traitlets import HasTraits, Unicode, observe

173

174

class Example(HasTraits):

175

value = Unicode()

176

177

@observe('value')

178

def _value_changed(self, change):

179

print("Change event details:")

180

print(f" name: {change['name']}") # 'value'

181

print(f" old: {change['old']}") # Previous value

182

print(f" new: {change['new']}") # New value

183

print(f" owner: {change['owner']}") # Self

184

print(f" type: {change['type']}") # 'change'

185

186

example = Example()

187

example.value = "initial" # old=u'', new='initial'

188

example.value = "updated" # old='initial', new='updated'

189

```

190

191

### Cross-Validation

192

193

```python

194

from traitlets import HasTraits, Int, TraitError, validate

195

196

class Rectangle(HasTraits):

197

width = Int(min=0)

198

height = Int(min=0)

199

max_area = Int(default_value=1000)

200

201

@validate('width', 'height')

202

def _validate_dimensions(self, proposal):

203

value = proposal['value']

204

trait_name = proposal['trait'].name

205

206

# Get the other dimension

207

if trait_name == 'width':

208

other_dim = self.height

209

else:

210

other_dim = self.width

211

212

# Check if area would exceed maximum

213

if value * other_dim > self.max_area:

214

raise TraitError(f"Area would exceed maximum of {self.max_area}")

215

216

return value

217

218

rect = Rectangle(max_area=100)

219

rect.width = 10

220

rect.height = 8 # 10 * 8 = 80, within limit

221

222

# rect.height = 15 # Would raise TraitError (10 * 15 = 150 > 100)

223

```

224

225

### Dynamic Defaults

226

227

```python

228

import time

229

from traitlets import HasTraits, Unicode, Float, default

230

231

class LogEntry(HasTraits):

232

message = Unicode()

233

timestamp = Float()

234

hostname = Unicode()

235

236

@default('timestamp')

237

def _default_timestamp(self):

238

return time.time()

239

240

@default('hostname')

241

def _default_hostname(self):

242

import socket

243

return socket.gethostname()

244

245

# Each instance gets current timestamp and hostname

246

entry1 = LogEntry(message="First log")

247

time.sleep(0.1)

248

entry2 = LogEntry(message="Second log")

249

250

print(entry1.timestamp != entry2.timestamp) # True - different times

251

print(entry1.hostname == entry2.hostname) # True - same host

252

```

253

254

### Conditional Default Values

255

256

```python

257

from traitlets import HasTraits, Unicode, Bool, default

258

259

class User(HasTraits):

260

username = Unicode()

261

email = Unicode()

262

is_admin = Bool(default_value=False)

263

display_name = Unicode()

264

265

@default('display_name')

266

def _default_display_name(self):

267

if self.email:

268

return self.email.split('@')[0]

269

elif self.username:

270

return self.username.title()

271

else:

272

return "Anonymous"

273

274

user1 = User(email="alice@example.com")

275

print(user1.display_name) # "alice"

276

277

user2 = User(username="bob_smith")

278

print(user2.display_name) # "Bob_Smith"

279

280

user3 = User()

281

print(user3.display_name) # "Anonymous"

282

```

283

284

### Validation with Modification

285

286

```python

287

from traitlets import HasTraits, Unicode, validate

288

289

class NormalizedText(HasTraits):

290

text = Unicode()

291

292

@validate('text')

293

def _validate_text(self, proposal):

294

value = proposal['value']

295

296

# Normalize whitespace and case

297

normalized = ' '.join(value.split()).lower()

298

299

# Remove forbidden characters

300

forbidden = ['<', '>', '&']

301

for char in forbidden:

302

normalized = normalized.replace(char, '')

303

304

return normalized

305

306

text_obj = NormalizedText()

307

text_obj.text = " Hello WORLD <script> "

308

print(text_obj.text) # "hello world script"

309

```

310

311

### Observer for All Traits

312

313

```python

314

from traitlets import HasTraits, Unicode, Int, observe, All

315

316

class Monitored(HasTraits):

317

name = Unicode()

318

value = Int()

319

description = Unicode()

320

321

@observe(All) # Observe all traits

322

def _any_trait_changed(self, change):

323

print(f"Any trait changed: {change['name']} = {change['new']}")

324

325

@observe(All, type='change') # Explicit change type

326

def _log_changes(self, change):

327

import datetime

328

timestamp = datetime.datetime.now().isoformat()

329

print(f"[{timestamp}] {change['name']}: {change['old']} -> {change['new']}")

330

331

obj = Monitored()

332

obj.name = "test" # Triggers both observers

333

obj.value = 42 # Triggers both observers

334

obj.description = "demo" # Triggers both observers

335

```

336

337

### Complex Validation Logic

338

339

```python

340

from traitlets import HasTraits, Unicode, Int, List, TraitError, validate, observe

341

342

class Project(HasTraits):

343

name = Unicode()

344

priority = Int(min=1, max=5)

345

tags = List(Unicode())

346

assignees = List(Unicode())

347

348

@validate('name')

349

def _validate_name(self, proposal):

350

name = proposal['value']

351

352

# Must be non-empty and alphanumeric with underscores

353

if not name or not name.replace('_', '').isalnum():

354

raise TraitError("Name must be alphanumeric with underscores only")

355

356

return name.lower() # Normalize to lowercase

357

358

@validate('tags')

359

def _validate_tags(self, proposal):

360

tags = proposal['value']

361

362

# Remove duplicates and normalize

363

normalized_tags = list(set(tag.lower().strip() for tag in tags))

364

365

# Limit to 5 tags maximum

366

if len(normalized_tags) > 5:

367

raise TraitError("Maximum 5 tags allowed")

368

369

return normalized_tags

370

371

@observe('priority')

372

def _priority_changed(self, change):

373

if change['new'] >= 4:

374

print(f"High priority project: {self.name}")

375

376

project = Project()

377

project.name = "My_Project_123" # Becomes "my_project_123"

378

project.priority = 4 # Triggers high priority message

379

project.tags = ["Python", "WEB", "python", "api", "REST"] # Normalized and deduplicated

380

print(project.tags) # ['python', 'web', 'api', 'rest']

381

```